<commit_before>'use strict';

var request = require('request');
var cheerio = require('cheerio');
var qs = require('querystring');
var url = require('url');

/**
 * @param {string} location
 * @param {object=} options
 */
function Showtimes(location, options) {
  if (!(this instanceof Showtimes)) {
    return new Showtimes(location, options);
  }

  this.userAgent = 'showtimes (http://github.com/jonursenbach/showtimes)';
  this.baseUrl = 'http://google.com/movies';
  this.location = location;

  var reserved = Object.keys(Showtimes.prototype);
  for (var i in options) {
    if (reserved.indexOf(i) === -1) {
      this[i] = options[i];
    }
  }
}

/**
 * @param {function} cb - Callback to handle the resulting theaters.
 * @param {number=} [page=1] - Page to pull theaters from. Hidden API and used during pagination.
 * @param {object=} [theaters=[]] - Current theaters object. Hidden API and used during pagination.
 * @returns {object}
 */
Showtimes.prototype.getTheaters = function (cb) {
  var self = this;
  var page = 1;
  var theaters = [];

  if (arguments.length > 1) {
    page = arguments[1];
    theaters = arguments[2];
  }

  var options = {
    url: self.baseUrl,
    qs: {
      hl: (typeof self.lang !== 'undefined') ? self.lang : "en",
      near: self.location,
      date: (typeof self.date !== 'undefined') ? self.date : 0,
      start: ((page - 1) * 10)
    },
    headers: {
      'User-Agent': self.userAgent,
      'gzip':true
    },
    encoding:'binary'
  };

  request(options, function (error, response, body) {
    if (error || response.statusCode !== 200) {
      if (error === null) {
        cb('Unknown error occured while querying theater data from Google Movies.');
      } else {
        cb(error);
      }

      return;
    }

    var $ = cheerio.load(body);

    var cloakedUrl;
    var genre;
    var imdb;
    var info;
    var match;
    var meridiem;
    var movieId;
    var rating;
    var runtime;
    var showtime;
    var showtimes;
    var theaterId;
    var theaterData;
    var trailer;

    if ($('.theater').length === 0) {
      cb($('#results').text());
      return;
    }

    $('.theater').each(function (i, theater) {
      theater = $(theater);

      cloakedUrl = theater.find('.desc h2.name a').attr('href');
      theaterId = cloakedUrl ? qs.parse(url.parse(cloakedUrl).query).tid : '';

      info = theater.find('.desc .info').text().split(' - ');

      theaterData = {
        id: theaterId,
        name: theater.find('.desc h2.name').text(),
        address: info[0] ? info[0].trim() : '',
        phoneNumber: info[1] ? info[1].trim() : '',
        movies: []
      };

      theater.find('.showtimes .movie').each(function (j, movie) {
        movie = $(movie);

        cloakedUrl = movie.find('.name a').attr('href');
        movieId = qs.parse(url.parse(cloakedUrl).query).mid;

        // Movie info format: RUNTIME - RATING - GENRE - TRAILER - IMDB
        // Some movies don't have a rating, trailer, or IMDb pages, so we need
        // to account for that.
        info = movie.find('.info').text().split(' - ');
        if (info[0].match(/(hr |min)/)) {
          runtime = info[0].trim();
          if(!info[1]){
            info[1] = "";
          }
          if (info[1].match(/Rated/)) {
            rating = info[1].replace(/Rated/, '').trim();
            if (typeof info[2] !== 'undefined') {
              if (info[2].match(/(IMDB|Trailer)/i)) {
                genre = false;
              } else {
                genre = info[2].trim();
              }
            } else {
              genre = false;
            }
          } else {
            rating = false;

            if (info[1].match(/(IMDB|Trailer)/i)) {
              genre = false;
            } else {
              genre = info[1].trim();
            }
          }
        } else {
          runtime = false;
          rating = false;
          genre = info[0].trim();
        }

        if (movie.find('.info a:contains("Trailer")').length) {
          cloakedUrl = 'https://google.com' + movie.find('.info a:contains("Trailer")').attr('href');
          trailer = qs.parse(url.parse(cloakedUrl).query).q;
        } else {
          trailer = false;
        }

        if (movie.find('.info a:contains("IMDb")').length) {
          cloakedUrl = 'https://google.com' + movie.find('.info a:contains("IMDb")').attr('href');
          imdb = qs.parse(url.parse(cloakedUrl).query).q;
        } else {
          imdb = false;
        }

        var movieData = {
          id: movieId,
          name: movie.find('.name').text(),
          runtime: runtime,
          rating: rating,
          genre: genre,
          imdb: imdb,
          trailer: trailer,
          showtimes: []
        };

        // Remove non-ASCII characters.
        if (movieData.runtime) {
          movieData.runtime = movieData.runtime.replace(/[^\x00-\x7F]/g, '').trim();
        }

        if (movieData.rating) {
          movieData.rating = movieData.rating.replace(/[^\x00-\x7F]/g, '').trim();
        }

        if (movieData.genre) {
          movieData.genre = movieData.genre.replace(/[^\x00-\x7F]/g, '').trim();
        }

        // Google displays showtimes like "10:00  11:20am  1:00  2:20  4:00  5:10  6:50  8:10  9:40  10:55pm". Since
        // they don't always apply am/pm to times, we need to run through the showtimes in reverse and then apply the
        // previous (later) meridiem to the next (earlier) movie showtime so we end up with something like
        // ["10:00am", "11:20am", "1:00pm", ...].
        showtimes = movie.find('.times').text().split(' ');
        meridiem = false;

        showtimes = showtimes.reverse();
        for (var x in showtimes) {
          // Remove non-ASCII characters.
          showtime = showtimes[x].replace(/[^\x00-\x7F]/g, '').trim();
          match = showtime.match(/(am|pm)/);
          if (match) {
            meridiem = match[0];
          } else if (meridiem) {
            showtime += meridiem;
          }

          showtimes[x] = showtime;
        }

        showtimes = showtimes.reverse();
        for (x in showtimes) {
          movieData.showtimes.push(showtimes[x].trim());
        }

        theaterData.movies.push(movieData);
      });

      theaters.push(theaterData);
    });

    // No pages to paginate, so return the theaters back.
    if ($('#navbar td a:contains("Next")').length === 0) {
      cb(null, theaters);
      return;
    }

    // Use the hidden API of getTheaters to pass in the next page and current
    // theaters.
    self.getTheaters(cb, ++page, theaters);
  });
};

/**
 * @param {function} cb - Callback to handle the resulting movie object.
 * @returns {object}
 */
Showtimes.prototype.getMovie = function (mid, cb) {
  var self = this;

  var options = {
    url: self.baseUrl,
    qs: {
      near: self.location,
      mid: mid,
      date: (typeof self.date !== 'undefined') ? self.date : 0
    },
    headers: {
      'User-Agent': self.userAgent
    }
  };
  request(options, function (error, response, body) {

    if (error || response.statusCode !== 200) {
      if (error === null) {
        cb('Unknown error occured while querying theater data from Google Movies.');
      } else {
        cb(error);
      }

      return;
    }
    var $ = cheerio.load(body);

    var cloakedUrl;
    var genre;
    var imdb;
    var rating;
    var runtime;
    var trailer;
    var director;
    var cast;
    var description;
    var info;
    var match;
    var meridiem;
    var showtime;
    var showtimes;
    var theaterId;
    var theaterData;

    if (!$('.showtimes')) {
      cb($('#results'));
      return;
    }

    var movie = $('.movie');

    // Movie info format: RUNTIME - RATING - GENRE - TRAILER - IMDB
    // Some movies don't have a rating, trailer, or IMDb pages, so we need
    // to account for that.
    // There is a br dividing the info from the director and actor info. Replacing it with
    // a new line makes it easier to split

    movie.find('.desc .info').not('.info.links').find('> br').replaceWith('\n');
    var infoArray = movie.find('.desc .info').not('.info.links').text().split('\n');
    info = infoArray[0].split(' - ');
    if (info[0].match(/(hr |min)/)) {
      runtime = info[0].trim();
      if (info[1].match(/Rated/)) {
        rating = info[1].replace(/Rated/, '').trim();
        if (typeof info[2] !== 'undefined') {
          if (info[2].match(/(IMDB|Trailer)/i)) {
            genre = false;
          } else {
            genre = info[2].trim();
          }
        } else {
          genre = false;
        }
      } else {
        rating = false;

        if (info[1].match(/(IMDB|Trailer)/i)) {
          genre = false;
        } else {
          genre = info[1].trim();
        }
      }
    } else {
      runtime = false;
      rating = false;
      genre = info[0].trim();
    }

    info = infoArray[1] ? infoArray[1].split(' - ') : undefined;
    if (info) {
      if (info[0].match(/Director:/)) {
        director = info[0].replace(/Director:/, '').trim();
      }
      if (info[1].match(/Cast:/)) {
        cast = info[1].replace(/Cast:/, '').trim().split(', ');
      }
    }

    // Longer descriptions can be split between two spans and displays a more/less link
    description = movie.find('span[itemprop="description"]').text();
    movie.find('#SynopsisSecond0').children().last().remove();
    description = description + movie.find('#SynopsisSecond0').text();
    description.replace('/"/', '');
    description = description.trim();

    if (movie.find('.info.links a:contains("Trailer")').length) {
      cloakedUrl = 'https://google.com' + movie.find('.info a:contains("Trailer")').attr('href');
      trailer = qs.parse(url.parse(cloakedUrl).query).q;
    } else {
      trailer = false;
    }

    if (movie.find('.info.links a:contains("IMDb")').length) {
      cloakedUrl = 'https://google.com' + movie.find('.info a:contains("IMDb")').attr('href');
      imdb = qs.parse(url.parse(cloakedUrl).query).q;
    } else {
      imdb = false;
    }

    var movieData = {
      id: mid,
      name: movie.find('h2[itemprop="name"]').text(),
      runtime: runtime,
      rating: rating,
      genre: genre,
      imdb: imdb,
      trailer: trailer,
      director: director,
      cast: cast,
      description: description,
      theaters: []
    };

    // Remove non-ASCII characters.
    if (movieData.runtime) {
      movieData.runtime = movieData.runtime.replace(/[^\x00-\x7F]/g, '').trim();
    }

    if (movieData.rating) {
      movieData.rating = movieData.rating.replace(/[^\x00-\x7F]/g, '').trim();
    }

    if (movieData.genre) {
      movieData.genre = movieData.genre.replace(/[^\x00-\x7F]/g, '').trim();
    }

    $('.theater').each(function (i, theater) {
      theater = $(theater);
      cloakedUrl = theater.find('.name a').attr('href');
      theaterId = cloakedUrl ? qs.parse(url.parse(cloakedUrl).query).tid : '';

      theaterData = {
        id: theaterId,
        name: theater.find('.name').text(),
        address: theater.find('.address').text(),
        showtimes: []
      };

      // Google displays showtimes like "10:00  11:20am  1:00  2:20  4:00  5:10  6:50  8:10  9:40  10:55pm". Since
      // they don't always apply am/pm to times, we need to run through the showtimes in reverse and then apply the
      // previous (later) meridiem to the next (earlier) movie showtime so we end up with something like
      // ["10:00am", "11:20am", "1:00pm", ...].
      showtimes = theater.find('.times').text().split(' ');
      meridiem = false;

      showtimes = showtimes.reverse();
      for (var x in showtimes) {
        // Remove non-ASCII characters.
        showtime = showtimes[x].replace(/[^\x00-\x7F]/g, '').trim();
        match = showtime.match(/(am|pm)/);
        if (match) {
          meridiem = match[0];
        } else {
          showtime += meridiem;
        }

        showtimes[x] = showtime;
      }

      showtimes = showtimes.reverse();
      for (x in showtimes) {
        theaterData.showtimes.push(showtimes[x].trim());
      }
      movieData.theaters.push(theaterData);
    });

    cb(null, movieData);
    return;
  });
};

module.exports = Showtimes;
<commit_msg>Fixing some code styling issues.<commit_after>'use strict';

var request = require('request');
var cheerio = require('cheerio');
var qs = require('querystring');
var url = require('url');

/**
 * @param {string} location
 * @param {object=} options
 */
function Showtimes(location, options) {
  if (!(this instanceof Showtimes)) {
    return new Showtimes(location, options);
  }

  this.userAgent = 'showtimes (http://github.com/jonursenbach/showtimes)';
  this.baseUrl = 'http://google.com/movies';
  this.location = location;

  var reserved = Object.keys(Showtimes.prototype);
  for (var i in options) {
    if (reserved.indexOf(i) === -1) {
      this[i] = options[i];
    }
  }
}

/**
 * @param {function} cb - Callback to handle the resulting theaters.
 * @param {number=} [page=1] - Page to pull theaters from. Hidden API and used during pagination.
 * @param {object=} [theaters=[]] - Current theaters object. Hidden API and used during pagination.
 * @returns {object}
 */
Showtimes.prototype.getTheaters = function (cb) {
  var self = this;
  var page = 1;
  var theaters = [];

  if (arguments.length > 1) {
    page = arguments[1];
    theaters = arguments[2];
  }

  var options = {
    url: self.baseUrl,
    qs: {
      hl: (typeof self.lang !== 'undefined') ? self.lang : "en",
      near: self.location,
      date: (typeof self.date !== 'undefined') ? self.date : 0,
      start: ((page - 1) * 10)
    },
    headers: {
      'User-Agent': self.userAgent,
      'gzip': true
    },
    encoding: 'binary'
  };

  request(options, function (error, response, body) {
    if (error || response.statusCode !== 200) {
      if (error === null) {
        cb('Unknown error occured while querying theater data from Google Movies.');
      } else {
        cb(error);
      }

      return;
    }

    var $ = cheerio.load(body);

    var cloakedUrl;
    var genre;
    var imdb;
    var info;
    var match;
    var meridiem;
    var movieId;
    var rating;
    var runtime;
    var showtime;
    var showtimes;
    var theaterId;
    var theaterData;
    var trailer;

    if ($('.theater').length === 0) {
      cb($('#results').text());
      return;
    }

    $('.theater').each(function (i, theater) {
      theater = $(theater);

      cloakedUrl = theater.find('.desc h2.name a').attr('href');
      theaterId = cloakedUrl ? qs.parse(url.parse(cloakedUrl).query).tid : '';

      info = theater.find('.desc .info').text().split(' - ');

      theaterData = {
        id: theaterId,
        name: theater.find('.desc h2.name').text(),
        address: info[0] ? info[0].trim() : '',
        phoneNumber: info[1] ? info[1].trim() : '',
        movies: []
      };

      theater.find('.showtimes .movie').each(function (j, movie) {
        movie = $(movie);

        cloakedUrl = movie.find('.name a').attr('href');
        movieId = qs.parse(url.parse(cloakedUrl).query).mid;

        // Movie info format: RUNTIME - RATING - GENRE - TRAILER - IMDB
        // Some movies don't have a rating, trailer, or IMDb pages, so we need
        // to account for that.
        info = movie.find('.info').text().split(' - ');
        if (info[0].match(/(hr |min)/)) {
          runtime = info[0].trim();
          if(!info[1]){
            info[1] = "";
          }
          if (info[1].match(/Rated/)) {
            rating = info[1].replace(/Rated/, '').trim();
            if (typeof info[2] !== 'undefined') {
              if (info[2].match(/(IMDB|Trailer)/i)) {
                genre = false;
              } else {
                genre = info[2].trim();
              }
            } else {
              genre = false;
            }
          } else {
            rating = false;

            if (info[1].match(/(IMDB|Trailer)/i)) {
              genre = false;
            } else {
              genre = info[1].trim();
            }
          }
        } else {
          runtime = false;
          rating = false;
          genre = info[0].trim();
        }

        if (movie.find('.info a:contains("Trailer")').length) {
          cloakedUrl = 'https://google.com' + movie.find('.info a:contains("Trailer")').attr('href');
          trailer = qs.parse(url.parse(cloakedUrl).query).q;
        } else {
          trailer = false;
        }

        if (movie.find('.info a:contains("IMDb")').length) {
          cloakedUrl = 'https://google.com' + movie.find('.info a:contains("IMDb")').attr('href');
          imdb = qs.parse(url.parse(cloakedUrl).query).q;
        } else {
          imdb = false;
        }

        var movieData = {
          id: movieId,
          name: movie.find('.name').text(),
          runtime: runtime,
          rating: rating,
          genre: genre,
          imdb: imdb,
          trailer: trailer,
          showtimes: []
        };

        // Remove non-ASCII characters.
        if (movieData.runtime) {
          movieData.runtime = movieData.runtime.replace(/[^\x00-\x7F]/g, '').trim();
        }

        if (movieData.rating) {
          movieData.rating = movieData.rating.replace(/[^\x00-\x7F]/g, '').trim();
        }

        if (movieData.genre) {
          movieData.genre = movieData.genre.replace(/[^\x00-\x7F]/g, '').trim();
        }

        // Google displays showtimes like "10:00  11:20am  1:00  2:20  4:00  5:10  6:50  8:10  9:40  10:55pm". Since
        // they don't always apply am/pm to times, we need to run through the showtimes in reverse and then apply the
        // previous (later) meridiem to the next (earlier) movie showtime so we end up with something like
        // ["10:00am", "11:20am", "1:00pm", ...].
        showtimes = movie.find('.times').text().split(' ');
        meridiem = false;

        showtimes = showtimes.reverse();
        for (var x in showtimes) {
          // Remove non-ASCII characters.
          showtime = showtimes[x].replace(/[^\x00-\x7F]/g, '').trim();
          match = showtime.match(/(am|pm)/);
          if (match) {
            meridiem = match[0];
          } else if (meridiem) {
            showtime += meridiem;
          }

          showtimes[x] = showtime;
        }

        showtimes = showtimes.reverse();
        for (x in showtimes) {
          movieData.showtimes.push(showtimes[x].trim());
        }

        theaterData.movies.push(movieData);
      });

      theaters.push(theaterData);
    });

    // No pages to paginate, so return the theaters back.
    if ($('#navbar td a:contains("Next")').length === 0) {
      cb(null, theaters);
      return;
    }

    // Use the hidden API of getTheaters to pass in the next page and current
    // theaters.
    self.getTheaters(cb, ++page, theaters);
  });
};

/**
 * @param {function} cb - Callback to handle the resulting movie object.
 * @returns {object}
 */
Showtimes.prototype.getMovie = function (mid, cb) {
  var self = this;

  var options = {
    url: self.baseUrl,
    qs: {
      near: self.location,
      mid: mid,
      date: (typeof self.date !== 'undefined') ? self.date : 0
    },
    headers: {
      'User-Agent': self.userAgent
    }
  };
  request(options, function (error, response, body) {

    if (error || response.statusCode !== 200) {
      if (error === null) {
        cb('Unknown error occured while querying theater data from Google Movies.');
      } else {
        cb(error);
      }

      return;
    }
    var $ = cheerio.load(body);

    var cloakedUrl;
    var genre;
    var imdb;
    var rating;
    var runtime;
    var trailer;
    var director;
    var cast;
    var description;
    var info;
    var match;
    var meridiem;
    var showtime;
    var showtimes;
    var theaterId;
    var theaterData;

    if (!$('.showtimes')) {
      cb($('#results'));
      return;
    }

    var movie = $('.movie');

    // Movie info format: RUNTIME - RATING - GENRE - TRAILER - IMDB
    // Some movies don't have a rating, trailer, or IMDb pages, so we need
    // to account for that.
    // There is a br dividing the info from the director and actor info. Replacing it with
    // a new line makes it easier to split

    movie.find('.desc .info').not('.info.links').find('> br').replaceWith('\n');
    var infoArray = movie.find('.desc .info').not('.info.links').text().split('\n');
    info = infoArray[0].split(' - ');
    if (info[0].match(/(hr |min)/)) {
      runtime = info[0].trim();
      if (info[1].match(/Rated/)) {
        rating = info[1].replace(/Rated/, '').trim();
        if (typeof info[2] !== 'undefined') {
          if (info[2].match(/(IMDB|Trailer)/i)) {
            genre = false;
          } else {
            genre = info[2].trim();
          }
        } else {
          genre = false;
        }
      } else {
        rating = false;

        if (info[1].match(/(IMDB|Trailer)/i)) {
          genre = false;
        } else {
          genre = info[1].trim();
        }
      }
    } else {
      runtime = false;
      rating = false;
      genre = info[0].trim();
    }

    info = infoArray[1] ? infoArray[1].split(' - ') : undefined;
    if (info) {
      if (info[0].match(/Director:/)) {
        director = info[0].replace(/Director:/, '').trim();
      }
      if (info[1].match(/Cast:/)) {
        cast = info[1].replace(/Cast:/, '').trim().split(', ');
      }
    }

    // Longer descriptions can be split between two spans and displays a more/less link
    description = movie.find('span[itemprop="description"]').text();
    movie.find('#SynopsisSecond0').children().last().remove();
    description = description + movie.find('#SynopsisSecond0').text();
    description.replace('/"/', '');
    description = description.trim();

    if (movie.find('.info.links a:contains("Trailer")').length) {
      cloakedUrl = 'https://google.com' + movie.find('.info a:contains("Trailer")').attr('href');
      trailer = qs.parse(url.parse(cloakedUrl).query).q;
    } else {
      trailer = false;
    }

    if (movie.find('.info.links a:contains("IMDb")').length) {
      cloakedUrl = 'https://google.com' + movie.find('.info a:contains("IMDb")').attr('href');
      imdb = qs.parse(url.parse(cloakedUrl).query).q;
    } else {
      imdb = false;
    }

    var movieData = {
      id: mid,
      name: movie.find('h2[itemprop="name"]').text(),
      runtime: runtime,
      rating: rating,
      genre: genre,
      imdb: imdb,
      trailer: trailer,
      director: director,
      cast: cast,
      description: description,
      theaters: []
    };

    // Remove non-ASCII characters.
    if (movieData.runtime) {
      movieData.runtime = movieData.runtime.replace(/[^\x00-\x7F]/g, '').trim();
    }

    if (movieData.rating) {
      movieData.rating = movieData.rating.replace(/[^\x00-\x7F]/g, '').trim();
    }

    if (movieData.genre) {
      movieData.genre = movieData.genre.replace(/[^\x00-\x7F]/g, '').trim();
    }

    $('.theater').each(function (i, theater) {
      theater = $(theater);
      cloakedUrl = theater.find('.name a').attr('href');
      theaterId = cloakedUrl ? qs.parse(url.parse(cloakedUrl).query).tid : '';

      theaterData = {
        id: theaterId,
        name: theater.find('.name').text(),
        address: theater.find('.address').text(),
        showtimes: []
      };

      // Google displays showtimes like "10:00  11:20am  1:00  2:20  4:00  5:10  6:50  8:10  9:40  10:55pm". Since
      // they don't always apply am/pm to times, we need to run through the showtimes in reverse and then apply the
      // previous (later) meridiem to the next (earlier) movie showtime so we end up with something like
      // ["10:00am", "11:20am", "1:00pm", ...].
      showtimes = theater.find('.times').text().split(' ');
      meridiem = false;

      showtimes = showtimes.reverse();
      for (var x in showtimes) {
        // Remove non-ASCII characters.
        showtime = showtimes[x].replace(/[^\x00-\x7F]/g, '').trim();
        match = showtime.match(/(am|pm)/);
        if (match) {
          meridiem = match[0];
        } else {
          showtime += meridiem;
        }

        showtimes[x] = showtime;
      }

      showtimes = showtimes.reverse();
      for (x in showtimes) {
        theaterData.showtimes.push(showtimes[x].trim());
      }
      movieData.theaters.push(theaterData);
    });

    cb(null, movieData);
    return;
  });
};

module.exports = Showtimes;
<|endoftext|>


<commit_before>angular.module('Aggie')

.controller('IncidentSelectModalController', [
  '$rootScope',
  '$scope',
  '$q',
  '$location',
  '$modal',
  '$modalStack',
  'Incident',
  'Report',
  'FlashService',
  function($rootScope, $scope, $q, $location, $modal, $modalStack, Incident, Report, flash) {

    $scope.setIncident = function(reports) {
      $modalStack.dismissAll();
      var modalInstance = $modal.open({
        windowClass: 'report-to-existing',
        size: 'lg',
        controller: 'IncidentSelectModalInstanceController',
        templateUrl: '/templates/incidents/report_incident_modal.html',
        resolve: {
          incidents: ['Incident', function(Incident) {
            return Incident.query().$promise;
          }],
          reports: function() {
            if (!reports) {
              reports = $scope.filterSelected($scope.reports);
            }

            return reports;
          }
        }
      });

      modalInstance.result.then(function(incidentId) {
        var ids = reports.map(function(report) {
          return report._id;
        });

        Report.linkToIncident({ ids: ids, incident: incidentId });
      });
    };
  }
])

.controller('IncidentSelectModalInstanceController', [
  '$rootScope',
  '$scope',
  '$modalInstance',
  'incidents',
  'reports',
  function($rootScope, $scope, $modalInstance, incidents, reports) {
    $scope.reports = angular.copy(reports);
    $scope.incidents = incidents.results;
    $scope.modal = $modalInstance;
    $scope._showErrors = false;

    $scope.select = function(incident) {
      // report._incident = incident._id;
      $modalInstance.close(incident._id);
    };

    $scope.showErrors = function() {
      return $scope._showErrors;
    };

    $scope.save = function(form) {
      if (form.$invalid) {
        $scope._showErrors = true;
        return;
      }

      $modalInstance.close($scope.reports);
    };

    $scope.close = function() {
      $modalInstance.dismiss('cancel');
    };

  }
]);
<commit_msg>5355: tags not being shown in modal: fixed<commit_after>angular.module('Aggie')

.controller('IncidentSelectModalController', [
  '$rootScope',
  '$scope',
  '$q',
  '$location',
  '$modal',
  '$modalStack',
  'Incident',
  'Report',
  'FlashService',
  function($rootScope, $scope, $q, $location, $modal, $modalStack, Incident, Report, flash) {

    $scope.setIncident = function(reports) {
      $modalStack.dismissAll();
      var modalInstance = $modal.open({
        windowClass: 'report-to-existing',
        size: 'lg',
        controller: 'IncidentSelectModalInstanceController',
        templateUrl: '/templates/incidents/report_incident_modal.html',
        resolve: {
          incidents: ['Incident', function(Incident) {
            return Incident.query().$promise;
          }],
          reports: function() {
            if (!reports) {
              reports = $scope.filterSelected($scope.reports);
            }

            return reports;
          }
        }
      });

      modalInstance.result.then(function(incidentId) {
        var ids = reports.map(function(report) {
          return report._id;
        });

        Report.linkToIncident({ ids: ids, incident: incidentId });
      });
    };
  }
])

.controller('IncidentSelectModalInstanceController', [
  '$rootScope',
  '$scope',
  '$modalInstance',
  'incidents',
  'reports',
  'Tags',
  function($rootScope, $scope, $modalInstance, incidents, reports, Tags) {
    $scope.reports = angular.copy(reports);
    $scope.incidents = incidents.results;
    $scope.modal = $modalInstance;
    $scope._showErrors = false;

    $scope.select = function(incident) {
      // report._incident = incident._id;
      $modalInstance.close(incident._id);
    };

    $scope.showErrors = function() {
      return $scope._showErrors;
    };

    $scope.save = function(form) {
      if (form.$invalid) {
        $scope._showErrors = true;
        return;
      }

      $modalInstance.close($scope.reports);
    };

    $scope.tagsToString = Tags.tagsToString;

    $scope.close = function() {
      $modalInstance.dismiss('cancel');
    };

  }
]);
<|endoftext|>


<commit_before>var rp = require('request-promise');
var _ = require('lodash');
var Promise = require('bluebird');

/**
 * Handles exchanges with MangopayAPI
 *
 * @constructor
 */
Mangopay = function(clientId, basicAuthKey) {
  this.baseUrl = 'https://api.sandbox.mangopay.com/v2/' + clientId;
  this.headers = {
    Authorization: 'Basic ' + basicAuthKey
  };
};

/**
 * Creates a Mangopay user
 *
 * @link https://docs.mangopay.com/api-references/users/natural-users/
 *
 * @param userData See API reference for required attributes
 *
 * @returns {Promise.<object>} A promise that resolves with created user data.
 *                             You need to handle the "Id" field and save it in local database.
 */
Mangopay.prototype.createUser = function(userData) {
  return rp.post({
    uri: this.baseUrl + '/users/natural',
    headers: this.headers,
    body: userData,
    json: true
  });
};

/**
 * Creates a wallet for the user
 *
 * @param userId Mangopay id for the user
 *
 * @returns @returns {Promise.<object>}
 */
Mangopay.prototype.createWallet = function(userId) {
  return rp.post({
    uri: this.baseUrl + '/wallets',
    headers: this.headers,
    body: {
      Owners: [userId],
      Description: 'Main wallet',
      Currency: 'EUR'
    },
    json: true
  });
};

/**
 * Preregisters the card and gives access to tokenization service.
 *
 * Use only if the @Mangopay.registerCard didn't complete successfully
 * and need to be amended.
 *
 * @link https://docs.mangopay.com/api-references/card-registration/
 *
 * @param mangopayUserId
 * @param cardInfo
 * @param cardInfo.currency ISO format currency string
 * @param cardInfo.cardType One of « CB_VISA_MASTERCARD », « MAESTRO » or « DINERS »
 *
 * @return {Promise.<object>} A promise that resolves with access to tokenization server and the id.
 */
Mangopay.prototype.preRegisterCard = function(mangopayUserId, cardInfo) {
  return rp.post({
    uri: this.baseUrl + '/cardregistrations',
    headers: this.headers,
    body: {
      UserId: mangopayUserId,
      Currency: cardInfo.currency,
      CardType: cardInfo.cardType
    },
    json: true
  });
};

/**
 * Calls the tokenization service to register the card.
 *
 * Use only if the @Mangopay.registerCard didn't complete successfully
 * and need to be amended.
 *
 * @link https://docs.mangopay.com/api-references/card-registration/
 *
 * @param cardData
 * @param cardData.cardNumber         The card number, without spaces or anything
 * @param cardData.cardExpirationDate The card expiration date in MMYY format.
 * @param cardData.cardCvx            The card security code
 *
 * @param preRegistrationResult       The result of preRegisterCard call
 *
 * @return {Promise.<string>} A promise resolved with data string confirming registration
 *                            or error=ERR_NUMBER if registration failed.
 */
Mangopay.prototype.tokenizeCard = function(cardData, preRegistrationResult) {
  return rp.post({
    uri: preRegistrationResult.CardRegistrationURL,
    form: _.assign(cardData, {
      accessKeyRef: preRegistrationResult.AccessKey,
      data: preRegistrationResult.PreregistrationData
    })
  });
};

/**
 * Updates Mangopay information with confirmation from tokenization service.
 *
 * Use only if the @Mangopay.registerCard didn't complete successfully
 * and need to be amended.
 *
 * @link https://docs.mangopay.com/api-references/card-registration/
 *
 * @param cardId
 * @param tokenizationData
 *
 * @returns {Promise.<object>}
 */
Mangopay.prototype.finaliseCardRegistration = function(cardId, tokenizationData) {
  return rp.post({
    uri: this.baseUrl + '/CardRegistrations/' + cardId,
    headers: this.headers,
    body: {
      RegistrationData: tokenizationData
    },
    json: true
  });
};

/**
 * Calls the chain of card registration.
 *
 * @param mangopayUserId
 * @param cardInfo                    A object containing the data inserted by user
 * @param cardInfo.type               One of « CB_VISA_MASTERCARD », « MAESTRO » or « DINERS »
 * @param cardInfo.currency           ISO currency identifier
 * @param cardInfo.cardNumber
 * @param cardInfo.cardExpirationDate In MMYY format
 * @param cardInfo.cardCvx
 *
 * @returns {Promise.<object>}        The card registration object with Status: VALIDATED if everything went OK.
 *                                    Note that this is not a Card, you get the CardId in response if you need
 *                                    to access the card directly
 */
Mangopay.prototype.registerCard = function(mangopayUserId, cardInfo) {
  var _this = this;

  return this.preRegisterCard(mangopayUserId, {
      currency: cardInfo.currency,
      cardType: cardInfo.type
    })
    .then(function(preRegistrationResult) {
      return [preRegistrationResult, _this.tokenizeCard({
        cardNumber: cardInfo.cardNumber,
        cardExpirationDate: cardInfo.cardExpirationDate,
        cardCvx: cardInfo.cardCvx
      }, preRegistrationResult)];
    })
    .spread(function(preRegistrationResult, tokenizationData) {
      if (tokenizationData.indexOf('error') !== -1) {
        return Promise.reject({ error: tokenizationData.split('=').pop() });
      }

      return _this.finaliseCardRegistration(preRegistrationResult.Id, tokenizationData);
    });
};

/**
 * Check if there is a existing credit card corresponding to cardId
 *
 * @param cardId
 */
Mangopay.prototype.existsCard = function(cardId) {
  var _this = this;

  return rp.get({
    uri: this.baseUrl + '/cards/' + cardId,
    headers: this.headers,
    json: true
  });
};

/**
 * Unactive the credit card corresponding to cardId
 *
 * @param cardId
 */
Mangopay.prototype.deleteCard = function(cardId) {
  var _this = this;

  return rp.put({
    uri: this.baseUrl + '/cards/' + cardId,
    headers: this.headers,
    body:{
      Active: false
    },
    json: true
  });
};

/**
 *
 *
 * @param mangopayUserId
 * @param bankAccount
 *
 * @returns {Promise.<object>}
 */
Mangopay.prototype.registerBankAccount = function(mangopayUserId, bankAccount) {
  return rp.post({
    uri: this.baseUrl + '/users/' + mangopayUserId + '/bankaccounts/' + bankAccount.Type,
    headers: this.headers,
    body: bankAccount,
    json: true
  });
};

/**
 * Retrieves the bank account for the given user
 *
 * @param mangopayUserId
 */
Mangopay.prototype.getBankAccount = function(mangopayUserId, accountId) {
  return rp.get({
    uri: this.baseUrl + '/users/' + mangopayUserId + '/bankaccounts/' + accountId,
    headers: this.headers,
    json: true
  });
};

/**
 * Debits fancookers credit card and puts the required amount in his wallet.
 *
 * @param fanCookerId
 * @param fanCookerCardId
 * @param amount            The value to debit, in eurocents
 * @param fanCookerWalletId
 * @param fees              The fees to debit, in eurocents
 *
 * @returns {Promise.<object>}
 */
Mangopay.prototype.debit = function(fanCookerId, fanCookerCardId, amount, fanCookerWalletId, fees = 0) {
  return rp.post({
    uri: this.baseUrl + '/payins/card/direct',
    headers: this.headers,
    body: {
      AuthorId: fanCookerId,
      CreditedUserId: fanCookerId,
      DebitedFunds: {
        Currency: 'EUR',
        Amount: amount
      },
      Fees: {
        Currency: 'EUR',
        Amount: fees
      },
      CreditedWalletId: fanCookerWalletId,
      SecureModeReturnUrl: 'http://www.bamlab.fr',
      CardId: fanCookerCardId
    },
    json: true
  });
};

/**
 * Transfers the money betweent two wallets inside Mangopay.
 * Should be called 48h after a transaction is concluded.
 *
 * @param fanCookerId
 * @param cookerId
 * @param amount
 * @param fanCookerWalletId
 * @param cookerWalletId
 *
 * @returns {Promise.<object>}
 */
Mangopay.prototype.transfer = function(fanCookerId, cookerId, amount, fees, fanCookerWalletId, cookerWalletId) {
  return rp.post({
    uri: this.baseUrl + '/transfers',
    headers: this.headers,
    body: {
      AuthorId: fanCookerId,
      CreditedUserId: cookerId,
      DebitedFunds: {
        Currency: 'EUR',
        Amount: amount
      },
      Fees: {
        Currency: 'EUR',
        Amount: fees
      },
      CreditedWalletId: cookerWalletId,
      DebitedWalletId: fanCookerWalletId
    },
    json: true
  });
};

/**
 * @param walletId
 *
 * @returns {Promise.<object>}
 */
Mangopay.prototype.getWallet = function(walletId) {
  return rp.get({
    uri: this.baseUrl + '/wallets/' + walletId,
    headers: this.headers,
    json: true
  });
};

/**
 * Transfers all the found from a wallet to the given bank account
 *
 * @param cookerId
 * @param cookerWalletId
 * @param accountId
 *
 * @returns {Promise.<object>}
 */
Mangopay.prototype.totalPayOut = function(cookerId, cookerWalletId, accountId) {
  var _this = this;

  return this.getWallet(cookerWalletId)
    .then(function(wallet) {
      var amount = wallet.Balance.Amount;

      return rp.post({
        uri: _this.baseUrl + '/payouts/bankwire',
        headers: _this.headers,
        body: {
          AuthorId: cookerId,
          DebitedWalletId: cookerWalletId,
          DebitedFunds: {
            Currency: 'EUR',
            Amount: amount
          },
          Fees: {
            Currency: 'EUR',
            Amount: 0
          },
          BankAccountId: accountId
        },
        json: true
      });
    });
};

Mangopay.prototype.getPayIn = function(payinId) {
  var _this = this;

  return rp.get({
    uri: this.baseUrl + '/payins/' + payinId,
    headers: this.headers,
    json: true
  });
};

module.exports = Mangopay;
<commit_msg>Changed coffee to js. Damn you Coffescript !<commit_after>var rp = require('request-promise');
var _ = require('lodash');
var Promise = require('bluebird');

/**
 * Handles exchanges with MangopayAPI
 *
 * @constructor
 */
Mangopay = function(clientId, basicAuthKey) {
  this.baseUrl = 'https://api.sandbox.mangopay.com/v2/' + clientId;
  this.headers = {
    Authorization: 'Basic ' + basicAuthKey
  };
};

/**
 * Creates a Mangopay user
 *
 * @link https://docs.mangopay.com/api-references/users/natural-users/
 *
 * @param userData See API reference for required attributes
 *
 * @returns {Promise.<object>} A promise that resolves with created user data.
 *                             You need to handle the "Id" field and save it in local database.
 */
Mangopay.prototype.createUser = function(userData) {
  return rp.post({
    uri: this.baseUrl + '/users/natural',
    headers: this.headers,
    body: userData,
    json: true
  });
};

/**
 * Creates a wallet for the user
 *
 * @param userId Mangopay id for the user
 *
 * @returns @returns {Promise.<object>}
 */
Mangopay.prototype.createWallet = function(userId) {
  return rp.post({
    uri: this.baseUrl + '/wallets',
    headers: this.headers,
    body: {
      Owners: [userId],
      Description: 'Main wallet',
      Currency: 'EUR'
    },
    json: true
  });
};

/**
 * Preregisters the card and gives access to tokenization service.
 *
 * Use only if the @Mangopay.registerCard didn't complete successfully
 * and need to be amended.
 *
 * @link https://docs.mangopay.com/api-references/card-registration/
 *
 * @param mangopayUserId
 * @param cardInfo
 * @param cardInfo.currency ISO format currency string
 * @param cardInfo.cardType One of « CB_VISA_MASTERCARD », « MAESTRO » or « DINERS »
 *
 * @return {Promise.<object>} A promise that resolves with access to tokenization server and the id.
 */
Mangopay.prototype.preRegisterCard = function(mangopayUserId, cardInfo) {
  return rp.post({
    uri: this.baseUrl + '/cardregistrations',
    headers: this.headers,
    body: {
      UserId: mangopayUserId,
      Currency: cardInfo.currency,
      CardType: cardInfo.cardType
    },
    json: true
  });
};

/**
 * Calls the tokenization service to register the card.
 *
 * Use only if the @Mangopay.registerCard didn't complete successfully
 * and need to be amended.
 *
 * @link https://docs.mangopay.com/api-references/card-registration/
 *
 * @param cardData
 * @param cardData.cardNumber         The card number, without spaces or anything
 * @param cardData.cardExpirationDate The card expiration date in MMYY format.
 * @param cardData.cardCvx            The card security code
 *
 * @param preRegistrationResult       The result of preRegisterCard call
 *
 * @return {Promise.<string>} A promise resolved with data string confirming registration
 *                            or error=ERR_NUMBER if registration failed.
 */
Mangopay.prototype.tokenizeCard = function(cardData, preRegistrationResult) {
  return rp.post({
    uri: preRegistrationResult.CardRegistrationURL,
    form: _.assign(cardData, {
      accessKeyRef: preRegistrationResult.AccessKey,
      data: preRegistrationResult.PreregistrationData
    })
  });
};

/**
 * Updates Mangopay information with confirmation from tokenization service.
 *
 * Use only if the @Mangopay.registerCard didn't complete successfully
 * and need to be amended.
 *
 * @link https://docs.mangopay.com/api-references/card-registration/
 *
 * @param cardId
 * @param tokenizationData
 *
 * @returns {Promise.<object>}
 */
Mangopay.prototype.finaliseCardRegistration = function(cardId, tokenizationData) {
  return rp.post({
    uri: this.baseUrl + '/CardRegistrations/' + cardId,
    headers: this.headers,
    body: {
      RegistrationData: tokenizationData
    },
    json: true
  });
};

/**
 * Calls the chain of card registration.
 *
 * @param mangopayUserId
 * @param cardInfo                    A object containing the data inserted by user
 * @param cardInfo.type               One of « CB_VISA_MASTERCARD », « MAESTRO » or « DINERS »
 * @param cardInfo.currency           ISO currency identifier
 * @param cardInfo.cardNumber
 * @param cardInfo.cardExpirationDate In MMYY format
 * @param cardInfo.cardCvx
 *
 * @returns {Promise.<object>}        The card registration object with Status: VALIDATED if everything went OK.
 *                                    Note that this is not a Card, you get the CardId in response if you need
 *                                    to access the card directly
 */
Mangopay.prototype.registerCard = function(mangopayUserId, cardInfo) {
  var _this = this;

  return this.preRegisterCard(mangopayUserId, {
      currency: cardInfo.currency,
      cardType: cardInfo.type
    })
    .then(function(preRegistrationResult) {
      return [preRegistrationResult, _this.tokenizeCard({
        cardNumber: cardInfo.cardNumber,
        cardExpirationDate: cardInfo.cardExpirationDate,
        cardCvx: cardInfo.cardCvx
      }, preRegistrationResult)];
    })
    .spread(function(preRegistrationResult, tokenizationData) {
      if (tokenizationData.indexOf('error') !== -1) {
        return Promise.reject({ error: tokenizationData.split('=').pop() });
      }

      return _this.finaliseCardRegistration(preRegistrationResult.Id, tokenizationData);
    });
};

/**
 * Check if there is a existing credit card corresponding to cardId
 *
 * @param cardId
 */
Mangopay.prototype.existsCard = function(cardId) {
  var _this = this;

  return rp.get({
    uri: this.baseUrl + '/cards/' + cardId,
    headers: this.headers,
    json: true
  });
};

/**
 * Unactive the credit card corresponding to cardId
 *
 * @param cardId
 */
Mangopay.prototype.deleteCard = function(cardId) {
  var _this = this;

  return rp.put({
    uri: this.baseUrl + '/cards/' + cardId,
    headers: this.headers,
    body:{
      Active: false
    },
    json: true
  });
};

/**
 *
 *
 * @param mangopayUserId
 * @param bankAccount
 *
 * @returns {Promise.<object>}
 */
Mangopay.prototype.registerBankAccount = function(mangopayUserId, bankAccount) {
  return rp.post({
    uri: this.baseUrl + '/users/' + mangopayUserId + '/bankaccounts/' + bankAccount.Type,
    headers: this.headers,
    body: bankAccount,
    json: true
  });
};

/**
 * Retrieves the bank account for the given user
 *
 * @param mangopayUserId
 */
Mangopay.prototype.getBankAccount = function(mangopayUserId, accountId) {
  return rp.get({
    uri: this.baseUrl + '/users/' + mangopayUserId + '/bankaccounts/' + accountId,
    headers: this.headers,
    json: true
  });
};

/**
 * Debits fancookers credit card and puts the required amount in his wallet.
 *
 * @param fanCookerId
 * @param fanCookerCardId
 * @param amount            The value to debit, in eurocents
 * @param fanCookerWalletId
 * @param fees              The fees to debit, in eurocents
 *
 * @returns {Promise.<object>}
 */
Mangopay.prototype.debit = function(fanCookerId, fanCookerCardId, amount, fanCookerWalletId, fees) {
  if (fees == null){
    fees = 0;
  }
  return rp.post({
    uri: this.baseUrl + '/payins/card/direct',
    headers: this.headers,
    body: {
      AuthorId: fanCookerId,
      CreditedUserId: fanCookerId,
      DebitedFunds: {
        Currency: 'EUR',
        Amount: amount
      },
      Fees: {
        Currency: 'EUR',
        Amount: fees
      },
      CreditedWalletId: fanCookerWalletId,
      SecureModeReturnUrl: 'http://www.bamlab.fr',
      CardId: fanCookerCardId
    },
    json: true
  });
};

/**
 * Transfers the money betweent two wallets inside Mangopay.
 * Should be called 48h after a transaction is concluded.
 *
 * @param fanCookerId
 * @param cookerId
 * @param amount
 * @param fanCookerWalletId
 * @param cookerWalletId
 *
 * @returns {Promise.<object>}
 */
Mangopay.prototype.transfer = function(fanCookerId, cookerId, amount, fees, fanCookerWalletId, cookerWalletId) {
  return rp.post({
    uri: this.baseUrl + '/transfers',
    headers: this.headers,
    body: {
      AuthorId: fanCookerId,
      CreditedUserId: cookerId,
      DebitedFunds: {
        Currency: 'EUR',
        Amount: amount
      },
      Fees: {
        Currency: 'EUR',
        Amount: fees
      },
      CreditedWalletId: cookerWalletId,
      DebitedWalletId: fanCookerWalletId
    },
    json: true
  });
};

/**
 * @param walletId
 *
 * @returns {Promise.<object>}
 */
Mangopay.prototype.getWallet = function(walletId) {
  return rp.get({
    uri: this.baseUrl + '/wallets/' + walletId,
    headers: this.headers,
    json: true
  });
};

/**
 * Transfers all the found from a wallet to the given bank account
 *
 * @param cookerId
 * @param cookerWalletId
 * @param accountId
 *
 * @returns {Promise.<object>}
 */
Mangopay.prototype.totalPayOut = function(cookerId, cookerWalletId, accountId) {
  var _this = this;

  return this.getWallet(cookerWalletId)
    .then(function(wallet) {
      var amount = wallet.Balance.Amount;

      return rp.post({
        uri: _this.baseUrl + '/payouts/bankwire',
        headers: _this.headers,
        body: {
          AuthorId: cookerId,
          DebitedWalletId: cookerWalletId,
          DebitedFunds: {
            Currency: 'EUR',
            Amount: amount
          },
          Fees: {
            Currency: 'EUR',
            Amount: 0
          },
          BankAccountId: accountId
        },
        json: true
      });
    });
};

Mangopay.prototype.getPayIn = function(payinId) {
  var _this = this;

  return rp.get({
    uri: this.baseUrl + '/payins/' + payinId,
    headers: this.headers,
    json: true
  });
};

module.exports = Mangopay;
<|endoftext|>


<commit_before>'use strict';

var path = require('path');
var archy = require('archy');
var isValid = require('is-valid-app');
var isObject = require('isobject');
var contents = require('file-contents');
var extend = require('extend-shallow');
var through = require('through2');
var write = require('write');
var File = require('vinyl');
var cache;

/**
 * Create file trees
 */

module.exports = function(config) {
  config = config || {};
  cache = cache || {};
  var namespace = config.name || 'default';
  cache[namespace] = cache[namespace] || {src: {}, dest: {}};

  function plugin(app) {
    if (!isValid(app, 'base-fs-tree')) return;

    var method = app.prePlugins ? 'prePlugins' : 'onStream';
    app[method](/./, function(file, next) {
      var opts = extend({}, config, app.options);
      if (opts.tree !== false && !file.isTree) {
        addFile(cache[namespace], file, {}, 'src');
      }
      next();
    });

    app.preWrite(/./, function(file, next) {
      var opts = extend({}, config, app.options);
      file.writeFile = !opts.treeOnly && file.isTree;
      next();
    });

    app.postWrite(/./, function(file, next) {
      var opts = extend({}, config, app.options);
      if (opts.tree !== false && !file.isTree) {
        addFile(cache[namespace], file, {}, 'dest');
      }
      next();
    });

    app.define('createTrees', function(options) {
      console.log(this.env)
      var opts = extend({name: 'default'}, config, app.options, options);
      if (opts.tree !== false) {
        writeFile(cache, 'dest', namespace, opts);
        writeFile(cache, 'src', namespace, opts);
      }
      // always reset the cache
      cache[namespace] = {src: {}, dest: {}};
    });

    return plugin;
  };

  plugin.capture = function(name, options) {
    if (isObject(name)) {
      options = name;
      name = null;
    }
    var opts = extend({name: name || 'default'}, config, options);
    var prop = opts.name;

    cache = cache || {};
    cache[prop] = {src: {}, dest: {}};

    return through.obj(function(file, enc, next) {
      if (opts.tree !== false) {
        file._isCaptured = true;
        addFile(cache[prop], file, opts, 'src');
      }
      file.writeFile = opts.treeOnly === false;
      next(null, file);
    });
  };

  plugin.create = function(name, options) {
    if (isObject(name)) {
      options = name;
      name = null;
    }

    var opts = extend({name: name || 'default'}, config, options);
    var prop = opts.name;

    cache = cache || {};
    cache[prop] || (cache[prop] = {src: {}, dest: {}});

    return through.obj(function(file, enc, next) {
      if (opts.tree !== false) {
        if (!file._isCaptured) {
          addFile(cache[prop], file, opts, 'src');
        }
        addFile(cache[prop], file, opts, 'dest');
      }
      file.writeFile = opts.treeOnly === false;
      next();
    }, function(next) {
      if (opts.tree !== false) {
        createFile(this, cache[prop], 'dest', prop, opts);
        createFile(this, cache[prop], 'src', prop, opts);
        cache[prop] = cache[prop] = {src: {}, dest: {}};
      }
      next();
    });
  };

  function createFile(stream, tree, name, prop, options) {
    var opts = extend({}, options);
    var str = create(tree[name], {label: 'cwd'});
    var file = new File({path: `${prop}-${name}.txt`, contents: new Buffer(str)});

    if (typeof opts.treename === 'function') {
      opts.treename(file);
    }

    file.writeFile = true;
    file.render = false;
    file.layout = null;
    file.isTree = true;
    contents.sync(file);
    stream.push(file);
  }

  function writeFile(obj, prop, namespace, options) {
    var opts = extend({}, config, options);
    var dest = path.resolve.bind(path, __dirname);
    var str = create(obj[namespace][prop], {label: namespace});

    if (opts.name !== namespace) {
      namespace = `${namespace}-${opts.name}`;
    }

    var destPath = opts.destPath || `${namespace}-${prop}.txt`;
    if (typeof opts.dest === 'string') {
      destPath = path.resolve(opts.dest, destPath);
    }

    var file = new File({path: destPath, contents: new Buffer(str)});
    contents.sync(file);

    if (typeof opts.treename === 'function') {
      opts.treename(file);
    }

    var destBase = file.dirname;
    if (typeof opts.dest === 'function') {
      destBase = path.resolve(opts.dest(file) || destBase);
    }
    destPath = path.resolve(destBase, file.path);
    write.sync(dest(destPath), file.contents.toString());
  }
  return plugin;
};

function addFile(tree, file, options, name) {
  if (isCached(file, name)) return;
  var opts = extend({label: 'cwd', prefix: ''}, options);
  var cwd = typeof opts.cwd === 'string' ? opts.cwd : process.cwd();
  addBranch(tree[name], path.join(opts.label, path.relative(cwd, file.path)));
}

function isCached(file, name) {
  if (!name && file._added) {
    return true;
  }
  if (name) {
    file._added = file._added || {};
    if (file._added[name]) {
      return true;
    }
    file._added[name] = true;
    return false;
  }
  file._added = true;
  return false;
}

function create(tree, options) {
  var opts = extend({label: 'cwd', prefix: ' '}, options);
  var obj = tree[opts.label] || tree.cwd;
  var archytree = createTree(obj, {}, opts.label);
  var str = archy(archytree, opts.prefix, opts);
  return str.replace(/^[^\n]+/, '.');
}

function addBranch(tree, path) {
  var segs = path.split(/[\\\/]/);
  var len = segs.length;
  var end = len - 1;
  var idx = -1;
  while (++idx < len) {
    if (idx === end) {
      tree[segs[idx]] = null;
    } else if (!tree[segs[idx]]) {
      tree[segs[idx]] = {};
    }
    tree = tree[segs[idx]];
  }
}

function createTree(tree, obj, label) {
  obj.label = label;
  for (var key in tree) {
    obj.nodes = obj.nodes || [];
    obj.nodes.push(tree[key] ? createTree(tree[key], {}, key) : key);
  }
  return obj;
}
<commit_msg>clean up<commit_after>'use strict';

var path = require('path');
var archy = require('archy');
var isValid = require('is-valid-app');
var isObject = require('isobject');
var contents = require('file-contents');
var extend = require('extend-shallow');
var through = require('through2');
var write = require('write');
var File = require('vinyl');
var cache;

/**
 * Create file trees
 */

module.exports = function(config) {
  config = config || {};
  cache = cache || {};
  var namespace = config.name || 'default';
  cache[namespace] = cache[namespace] || {src: {}, dest: {}};

  function plugin(app) {
    if (!isValid(app, 'base-fs-tree')) return;

    var method = app.prePlugins ? 'prePlugins' : 'onStream';
    app[method](/./, function(file, next) {
      var opts = extend({}, config, app.options);
      if (opts.tree !== false && !file.isTree) {
        addFile(cache[namespace], file, {}, 'src');
      }
      next();
    });

    app.preWrite(/./, function(file, next) {
      var opts = extend({}, config, app.options);
      file.writeFile = !opts.treeOnly && file.isTree;
      next();
    });

    app.postWrite(/./, function(file, next) {
      var opts = extend({}, config, app.options);
      if (opts.tree !== false && !file.isTree) {
        addFile(cache[namespace], file, {}, 'dest');
      }
      next();
    });

    app.define('createTrees', function(options) {
      var opts = extend({name: 'default'}, config, app.options, options);
      if (opts.tree !== false) {
        writeFile(cache, 'dest', namespace, opts);
        writeFile(cache, 'src', namespace, opts);
      }
      // always reset the cache
      cache[namespace] = {src: {}, dest: {}};
    });

    return plugin;
  };

  plugin.capture = function(name, options) {
    if (isObject(name)) {
      options = name;
      name = null;
    }
    var opts = extend({name: name || 'default'}, config, options);
    var prop = opts.name;

    cache = cache || {};
    cache[prop] = {src: {}, dest: {}};

    return through.obj(function(file, enc, next) {
      if (opts.tree !== false) {
        file._isCaptured = true;
        addFile(cache[prop], file, opts, 'src');
      }
      file.writeFile = opts.treeOnly === false;
      next(null, file);
    });
  };

  plugin.create = function(name, options) {
    if (isObject(name)) {
      options = name;
      name = null;
    }

    var opts = extend({name: name || 'default'}, config, options);
    var prop = opts.name;

    cache = cache || {};
    cache[prop] || (cache[prop] = {src: {}, dest: {}});

    return through.obj(function(file, enc, next) {
      if (opts.tree !== false) {
        if (!file._isCaptured) {
          addFile(cache[prop], file, opts, 'src');
        }
        addFile(cache[prop], file, opts, 'dest');
      }
      file.writeFile = opts.treeOnly === false;
      next();
    }, function(next) {
      if (opts.tree !== false) {
        createFile(this, cache[prop], 'dest', prop, opts);
        createFile(this, cache[prop], 'src', prop, opts);
        cache[prop] = cache[prop] = {src: {}, dest: {}};
      }
      next();
    });
  };

  function createFile(stream, tree, name, prop, options) {
    var opts = extend({}, options);
    var str = create(tree[name], {label: 'cwd'});
    var file = new File({path: `${prop}-${name}.txt`, contents: new Buffer(str)});

    if (typeof opts.treename === 'function') {
      opts.treename(file);
    }

    file.writeFile = true;
    file.render = false;
    file.layout = null;
    file.isTree = true;
    contents.sync(file);
    stream.push(file);
  }

  function writeFile(obj, prop, namespace, options) {
    var opts = extend({}, config, options);
    var dest = path.resolve.bind(path, __dirname);
    var str = create(obj[namespace][prop], {label: namespace});

    if (opts.name !== namespace) {
      namespace = `${namespace}-${opts.name}`;
    }

    var destPath = opts.destPath || `${namespace}-${prop}.txt`;
    if (typeof opts.dest === 'string') {
      destPath = path.resolve(opts.dest, destPath);
    }

    var file = new File({path: destPath, contents: new Buffer(str)});
    contents.sync(file);

    if (typeof opts.treename === 'function') {
      opts.treename(file);
    }

    var destBase = file.dirname;
    if (typeof opts.dest === 'function') {
      destBase = path.resolve(opts.dest(file) || destBase);
    }
    destPath = path.resolve(destBase, file.path);
    write.sync(dest(destPath), file.contents.toString());
  }
  return plugin;
};

function addFile(tree, file, options, name) {
  if (isCached(file, name)) return;
  var opts = extend({label: 'cwd', prefix: ''}, options);
  var cwd = typeof opts.cwd === 'string' ? opts.cwd : process.cwd();
  addBranch(tree[name], path.join(opts.label, path.relative(cwd, file.path)));
}

function isCached(file, name) {
  if (!name && file._added) {
    return true;
  }
  if (name) {
    file._added = file._added || {};
    if (file._added[name]) {
      return true;
    }
    file._added[name] = true;
    return false;
  }
  file._added = true;
  return false;
}

function create(tree, options) {
  var opts = extend({label: 'cwd', prefix: ' '}, options);
  var obj = tree[opts.label] || tree.cwd;
  var archytree = createTree(obj, {}, opts.label);
  var str = archy(archytree, opts.prefix, opts);
  return str.replace(/^[^\n]+/, '.');
}

function addBranch(tree, path) {
  var segs = path.split(/[\\\/]+/);
  var len = segs.length;
  var idx = -1;

  while (++idx < len) {
    if (!tree[segs[idx]]) {
      tree[segs[idx]] = {};
    } else if (idx === len - 1) {
      tree[segs[idx]] = null;
    }
    tree = tree[segs[idx]];
  }
}

function createTree(tree, obj, label) {
  obj.label = label;
  for (var key in tree) {
    obj.nodes = obj.nodes || [];
    obj.nodes.push(tree[key] ? createTree(tree[key], {}, key) : key);
  }
  return obj;
}
<|endoftext|>


<commit_before>define("ace/ext/themelist",["require","exports","module","ace/lib/fixoldbrowsers"],function(e,t,n){"use strict";e("ace/lib/fixoldbrowsers");var r=[["Chrome"],["Clouds"],["Crimson Editor"],["Dawn"],["Dreamweaver"],["Eclipse"],["GitHub"],["IPlastic"],["Solarized Light"],["TextMate"],["Tomorrow"],["XCode"],["Kuroir"],["KatzenMilch"],["SQL Server","sqlserver","light"],["Ambiance","ambiance","dark"],["Chaos","chaos","dark"],["Clouds Midnight","clouds_midnight","dark"],["Cobalt","cobalt","dark"],["idle Fingers","idle_fingers","dark"],["krTheme","kr_theme","dark"],["Merbivore","merbivore","dark"],["Merbivore Soft","merbivore_soft","dark"],["Mono Industrial","mono_industrial","dark"],["Monokai","monokai","dark"],["Pastel on dark","pastel_on_dark","dark"],["Solarized Dark","solarized_dark","dark"],["Terminal","terminal","dark"],["Tomorrow Night","tomorrow_night","dark"],["Tomorrow Night Blue","tomorrow_night_blue","dark"],["Tomorrow Night Bright","tomorrow_night_bright","dark"],["Tomorrow Night 80s","tomorrow_night_eighties","dark"],["Twilight","twilight","dark"],["Vibrant Ink","vibrant_ink","dark"]];t.themesByName={},t.themes=r.map(function(e){var n=e[1]||e[0].replace(/ /g,"_").toLowerCase(),r={caption:e[0],theme:"ace/theme/"+n,isDark:e[2]=="dark",name:n};return t.themesByName[n]=r,r})});
                (function() {
                    window.require(["ace/ext/themelist"], function() {});
                })();
            <commit_msg>Update public/libs/bower_components/ace-builds/src-min/ext-themelist.js<commit_after><|endoftext|>


<commit_before>// @flow
import * as Chat2Gen from '../chat2-gen'
import * as ConfigGen from '../config-gen'
import * as Constants from '../../constants/push'
import * as FsGen from '../../actions/fs-gen'
import * as FsConstants from '../../constants/fs'
import * as FsTypes from '../../constants/types/fs'
import * as NotificationsGen from '../../actions/notifications-gen'
import * as ProfileGen from '../profile-gen'
import * as PushGen from '../push-gen'
import * as PushNotifications from 'react-native-push-notification'
import * as RPCChatTypes from '../../constants/types/rpc-chat-gen'
import * as RPCTypes from '../../constants/types/rpc-gen'
import * as ChatTypes from '../../constants/types/chat2'
import * as Saga from '../../util/saga'
import * as WaitingGen from '../waiting-gen'
import * as RouteTreeGen from '../route-tree-gen'
import logger from '../../logger'
import {NativeModules, NativeEventEmitter} from 'react-native'
import {isIOS, isAndroid} from '../../constants/platform'

let lastCount = -1
const updateAppBadge = (_, action) => {
  const count = (action.payload.badgeState.conversations || []).reduce(
    (total, c) => (c.badgeCounts ? total + c.badgeCounts[`${RPCTypes.commonDeviceType.mobile}`] : total),
    0
  )

  PushNotifications.setApplicationIconBadgeNumber(count)
  // Only do this native call if the count actually changed, not over and over if its zero
  if (count === 0 && lastCount !== 0) {
    PushNotifications.cancelAllLocalNotifications()
  }
  lastCount = count
}

// Push notifications on android are very messy. It works differently if we're entirely killed or if we're in the background
// If we're killed it all works. clicking on the notification launches us and we get the onNotify callback and it all works
// If we're backgrounded we get the silent or the silent and real. To work around this we:
// 1. Plumb through the intent from the java side if we relaunch due to push
// 2. We store the last push and re-use it when this event is emitted to just 'rerun' the push
let lastPushForAndroid = null
const listenForNativeAndroidIntentNotifications = emitter => {
  const RNEmitter = new NativeEventEmitter(NativeModules.KeybaseEngine)
  // If android launched due to push
  RNEmitter.addListener('androidIntentNotification', (evt) => {
    logger.info('[PushAndroidIntent]', lastPushForAndroid && lastPushForAndroid.type)
    if (!lastPushForAndroid) {
      if (!evt) {
        return
      }
      lastPushForAndroid = Constants.normalizePush(evt)
      if (!lastPushForAndroid) {
        return
      }
    }

    switch (lastPushForAndroid.type) {
      // treat this like a loud message
      case 'chat.newmessageSilent_2':
        lastPushForAndroid = {
          conversationIDKey: lastPushForAndroid.conversationIDKey,
          membersType: lastPushForAndroid.membersType,
          type: 'chat.newmessage',
          unboxPayload: lastPushForAndroid.unboxPayload,
          userInteraction: true,
        }
        break
      case 'chat.newmessage':
        lastPushForAndroid.userInteraction = true
        break
      case 'follow':
        lastPushForAndroid.userInteraction = true
        break
      default:
        lastPushForAndroid = null
        return
    }

    emitter(PushGen.createNotification({notification: lastPushForAndroid}))
    lastPushForAndroid = null
  })

  // TODO: move this out of this file.
  RNEmitter.addListener('onShareData', evt => {
    logger.info('[ShareDataIntent]', evt)
    emitter(RouteTreeGen.createNavigateTo({path: FsConstants.fsRootRouteForNav1}))
    emitter(FsGen.createSetIncomingShareLocalPath({localPath: FsTypes.stringToLocalPath(evt.path)}))
    emitter(FsGen.createShowIncomingShare({initialDestinationParentPath: FsTypes.stringToPath('/keybase')}))
  })
  RNEmitter.addListener('onShareText', evt => {
    logger.info('[ShareTextIntent]', evt)
    emitter(RouteTreeGen.createNavigateTo({path: FsConstants.fsRootRouteForNav1}))
    // TODO: implement
  })
}

const listenForPushNotificationsFromJS = emitter => {
  const onRegister = token => {
    console.log('[PushToken] received new token: ', token)
    emitter(PushGen.createUpdatePushToken({token: token.token}))
  }

  const onNotification = n => {
    if (isAndroid && n.type === 'chat.newmessageSilent_2') {
      const convID = n.c
      const payload = n.m
      const membersType = parseInt(n.t)
      const displayPlaintext = n.n === 'true'
      const messageId = parseInt(n.d)
      const pushId = JSON.parse(n.p)[0]
      const badgeCount = parseInt(n.b)
      const unixTime = parseInt(n.x)
      const soundName = n.s
      NativeModules.PushHandler.handlePushNotification(
        convID,
        payload,
        membersType,
        displayPlaintext,
        messageId,
        pushId,
        badgeCount,
        unixTime,
        soundName,
      )
      return
    }
    const notification = Constants.normalizePush(n)
    if (!notification) {
      return
    }
    // bookkeep for android special handling
    lastPushForAndroid = notification
    emitter(PushGen.createNotification({notification}))
  }

  const onError = error => {
    logger.error('push error:', error)
  }

  PushNotifications.configure({
    onError,
    onNotification,
    onRegister,
    popInitialNotification: false,
    // Don't request permissions for ios, we'll ask later, after showing UI
    requestPermissions: !isIOS,
    senderID: Constants.androidSenderID,
  })
}

function* setupPushEventLoop() {
  const pushChannel = yield Saga.eventChannel(emitter => {
    if (!isIOS) {
      listenForNativeAndroidIntentNotifications(emitter)
    }
    listenForPushNotificationsFromJS(emitter)

    // we never unsubscribe
    return () => {}
  }, Saga.buffers.expanding(10))

  while (true) {
    const action = yield Saga.take(pushChannel)
    yield Saga.put(action)
  }
}

function* handleLoudMessage(notification) {
  // We only care if the user clicked while in session
  if (!notification.userInteraction) {
    return
  }

  const {conversationIDKey, unboxPayload, membersType} = notification

  yield Saga.put(Chat2Gen.createSelectConversation({conversationIDKey, reason: 'push'}))
  yield Saga.put(Chat2Gen.createNavigateToThread())
  if (unboxPayload && membersType && !isIOS) {
    logger.info('[Push] unboxing message')
    try {
      yield* Saga.callPromise(RPCChatTypes.localUnboxMobilePushNotificationRpcPromise, {
        convID: conversationIDKey,
        membersType,
        payload: unboxPayload,
        shouldAck: false,
      })
    } catch (e) {
      logger.info('[Push] failed to unbox message form payload')
    }
  }
}

// on iOS the go side handles a lot of push details
function* handlePush(_, action) {
  try {
    const notification = action.payload.notification
    logger.info('[Push]: ' + notification.type || 'unknown')

    switch (notification.type) {
      case 'chat.readmessage':
        logger.info('[Push] read message')
        if (notification.badges === 0) {
          PushNotifications.cancelAllLocalNotifications()
        }
        break
      case 'chat.newmessageSilent_2':
        // entirely handled by go on ios and in onNotification on Android
        break
      case 'chat.newmessage':
        yield* handleLoudMessage(notification)
        break
      case 'follow':
        // We only care if the user clicked while in session
        if (notification.userInteraction) {
          const {username} = notification
          logger.info('[Push] follower: ', username)
          yield Saga.put(ProfileGen.createShowUserProfile({username}))
        }
        break
      case 'chat.extension':
        const {conversationIDKey} = notification
        yield Saga.put(Chat2Gen.createSelectConversation({conversationIDKey, reason: 'extension'}))
        break
    }
  } catch (e) {
    if (__DEV__) {
      console.error(e)
    }

    logger.error('[Push] unhandled!!')
  }
}

const uploadPushToken = state =>
  !!state.push.token &&
  !!state.config.deviceID &&
  RPCTypes.apiserverPostRpcPromise({
    args: [
      {key: 'push_token', value: state.push.token},
      {key: 'device_id', value: state.config.deviceID},
      {key: 'token_type', value: Constants.tokenType},
    ],
    endpoint: 'device/push_token',
  })
    .then(() => {
      logger.info('[PushToken] Uploaded to server')
      return false
    })
    .catch(e => {
      logger.error("[PushToken] Couldn't save a push token", e)
    })

function* deletePushToken(state, action) {
  const waitKey = 'push:deleteToken'
  yield Saga.put(
    ConfigGen.createLogoutHandshakeWait({increment: true, name: waitKey, version: action.payload.version})
  )

  try {
    const deviceID = state.config.deviceID
    if (!deviceID) {
      logger.info('[PushToken] no device id')
      return
    }

    yield* Saga.callPromise(RPCTypes.apiserverDeleteRpcPromise, {
      args: [{key: 'device_id', value: deviceID}, {key: 'token_type', value: Constants.tokenType}],
      endpoint: 'device/push_token',
    })
    logger.info('[PushToken] deleted from server')
  } catch (e) {
    logger.error('[PushToken] delete failed', e)
  } finally {
    yield Saga.put(
      ConfigGen.createLogoutHandshakeWait({
        increment: false,
        name: waitKey,
        version: action.payload.version,
      })
    )
  }
}

const requestPermissionsFromNative = () =>
  isIOS ? PushNotifications.requestPermissions() : Promise.resolve()
const askNativeIfSystemPushPromptHasBeenShown = () =>
  isIOS ? NativeModules.PushPrompt.getHasShownPushPrompt() : Promise.resolve(false)
const checkPermissionsFromNative = () =>
  new Promise((resolve, reject) => PushNotifications.checkPermissions(resolve))
const monsterStorageKey = 'shownMonsterPushPrompt'

function* neverShowMonsterAgain(state) {
  if (!state.push.showPushPrompt) {
    yield Saga.spawn(() =>
      RPCTypes.configSetValueRpcPromise({path: `ui.${monsterStorageKey}`, value: {b: true, isNull: false}})
    )
  }
}

function* requestPermissions() {
  if (isIOS) {
    const shownPushPrompt = yield* Saga.callPromise(askNativeIfSystemPushPromptHasBeenShown)
    if (shownPushPrompt) {
      // we've already shown the prompt, take them to settings
      yield Saga.put(ConfigGen.createOpenAppSettings())
      yield Saga.put(PushGen.createShowPermissionsPrompt({show: false}))
      return
    }
  }
  try {
    yield Saga.put(WaitingGen.createIncrementWaiting({key: Constants.permissionsRequestingWaitingKey}))
    logger.info('[PushRequesting] asking native')
    const permissions = yield* Saga.callPromise(requestPermissionsFromNative)
    logger.info('[PushRequesting] after prompt:', permissions)
    if (permissions?.alert || permissions?.badge) {
      logger.info('[PushRequesting] enabled')
      yield Saga.put(PushGen.createUpdateHasPermissions({hasPermissions: true}))
    } else {
      logger.info('[PushRequesting] disabled')
      yield Saga.put(PushGen.createUpdateHasPermissions({hasPermissions: false}))
    }
  } finally {
    yield Saga.put(WaitingGen.createDecrementWaiting({key: Constants.permissionsRequestingWaitingKey}))
    yield Saga.put(PushGen.createShowPermissionsPrompt({show: false}))
  }
}

function* initialPermissionsCheck(): Saga.SagaGenerator<any, any> {
  const hasPermissions = yield _checkPermissions(null)
  if (hasPermissions) {
    // Get the token
    yield Saga.spawn(requestPermissionsFromNative)
  } else {
    const shownNativePushPromptTask = yield Saga._fork(askNativeIfSystemPushPromptHasBeenShown)
    const shownMonsterPushPromptTask = yield Saga._fork(() =>
      RPCTypes.configGetValueRpcPromise({path: `ui.${monsterStorageKey}`})
        .then(v => !!v.b)
        .catch(() => false)
    )
    const [shownNativePushPrompt, shownMonsterPushPrompt] = yield Saga.join(
      shownNativePushPromptTask,
      shownMonsterPushPromptTask
    )
    logger.info(
      '[PushInitialCheck] shownNativePushPrompt:',
      shownNativePushPrompt,
      'shownMonsterPushPrompt:',
      shownMonsterPushPrompt
    )
    if (!shownNativePushPrompt && !shownMonsterPushPrompt) {
      logger.info('[PushInitialCheck] no permissions, never shown prompt, now show prompt')
      yield Saga.put(PushGen.createShowPermissionsPrompt({show: true}))
    }
  }
}

function* checkPermissions(_, action: ConfigGen.MobileAppStatePayload | null) {
  yield* _checkPermissions(action)
}
// Call when we foreground and on app start, action is null on app start. Returns if you have permissions
function* _checkPermissions(action: ConfigGen.MobileAppStatePayload | null) {
  // Only recheck on foreground, not background
  if (action && action.payload.nextAppState !== 'active') {
    logger.info('[PushCheck] skip on backgrounding')
    return false
  }

  console.log('[PushCheck] checking ', action ? 'on foreground' : 'on startup')
  const permissions = yield* Saga.callPromise(checkPermissionsFromNative)
  if (permissions.alert || permissions.badge) {
    const state = yield* Saga.selectState()
    if (!state.push.hasPermissions) {
      logger.info('[PushCheck] enabled: getting token')
      yield Saga.put(PushGen.createUpdateHasPermissions({hasPermissions: true}))
      yield* Saga.callPromise(requestPermissionsFromNative)
    } else {
      logger.info('[PushCheck] enabled already')
    }
    return true
  } else {
    logger.info('[PushCheck] disabled')
    yield Saga.put(PushGen.createUpdateHasPermissions({hasPermissions: false}))
    return false
  }
}

const getStartupDetailsFromInitialPush = (): Promise<
  null | {startupFollowUser: string} | {startupConversation: ChatTypes.ConversationIDKey}
> =>
  new Promise(resolve => {
    PushNotifications.popInitialNotification(n => {
      const notification = Constants.normalizePush(n)
      if (!notification) {
        resolve(null)
        return
      }
      if (notification.type === 'follow') {
        if (notification.username) {
          resolve({startupFollowUser: notification.username})
          return
        }
      } else if (notification.type === 'chat.newmessage') {
        if (notification.conversationIDKey) {
          resolve({startupConversation: notification.conversationIDKey})
          return
        }
      }
      resolve(null)
    })
  })

function* pushSaga(): Saga.SagaGenerator<any, any> {
  // Permissions
  yield* Saga.chainGenerator<PushGen.RequestPermissionsPayload>(
    PushGen.requestPermissions,
    requestPermissions
  )
  yield* Saga.chainGenerator<PushGen.ShowPermissionsPromptPayload | PushGen.RejectPermissionsPayload>(
    [PushGen.showPermissionsPrompt, PushGen.rejectPermissions],
    neverShowMonsterAgain
  )
  yield* Saga.chainGenerator<ConfigGen.MobileAppStatePayload>(ConfigGen.mobileAppState, checkPermissions)

  // Token handling
  yield* Saga.chainAction<PushGen.UpdatePushTokenPayload | ConfigGen.BootstrapStatusLoadedPayload>(
    [PushGen.updatePushToken, ConfigGen.bootstrapStatusLoaded],
    uploadPushToken
  )
  yield* Saga.chainGenerator<ConfigGen.LogoutHandshakePayload>(ConfigGen.logoutHandshake, deletePushToken)

  yield* Saga.chainAction<NotificationsGen.ReceivedBadgeStatePayload>(
    NotificationsGen.receivedBadgeState,
    updateAppBadge
  )
  yield* Saga.chainGenerator<PushGen.NotificationPayload>(PushGen.notification, handlePush)
  yield* Saga.chainGenerator<ConfigGen.DaemonHandshakePayload>(ConfigGen.daemonHandshake, setupPushEventLoop)
  yield Saga.spawn(initialPermissionsCheck)
}

export default pushSaga
export {getStartupDetailsFromInitialPush}
<commit_msg>android: get rid of lastPushForAndroid<commit_after>// @flow
import * as Chat2Gen from '../chat2-gen'
import * as ConfigGen from '../config-gen'
import * as Constants from '../../constants/push'
import * as FsGen from '../../actions/fs-gen'
import * as FsConstants from '../../constants/fs'
import * as FsTypes from '../../constants/types/fs'
import * as NotificationsGen from '../../actions/notifications-gen'
import * as ProfileGen from '../profile-gen'
import * as PushGen from '../push-gen'
import * as PushNotifications from 'react-native-push-notification'
import * as RPCChatTypes from '../../constants/types/rpc-chat-gen'
import * as RPCTypes from '../../constants/types/rpc-gen'
import * as ChatTypes from '../../constants/types/chat2'
import * as Saga from '../../util/saga'
import * as WaitingGen from '../waiting-gen'
import * as RouteTreeGen from '../route-tree-gen'
import logger from '../../logger'
import {NativeModules, NativeEventEmitter} from 'react-native'
import {isIOS, isAndroid} from '../../constants/platform'

let lastCount = -1
const updateAppBadge = (_, action) => {
  const count = (action.payload.badgeState.conversations || []).reduce(
    (total, c) => (c.badgeCounts ? total + c.badgeCounts[`${RPCTypes.commonDeviceType.mobile}`] : total),
    0
  )

  PushNotifications.setApplicationIconBadgeNumber(count)
  // Only do this native call if the count actually changed, not over and over if its zero
  if (count === 0 && lastCount !== 0) {
    PushNotifications.cancelAllLocalNotifications()
  }
  lastCount = count
}

// Push notifications on android are very messy. It works differently if we're entirely killed or if we're in the background
// If we're killed it all works. clicking on the notification launches us and we get the onNotify callback and it all works
// If we're backgrounded we get the silent or the silent and real. To work around this we:
// 1. Plumb through the intent from the java side if we relaunch due to push
// 2. We store the last push and re-use it when this event is emitted to just 'rerun' the push
const listenForNativeAndroidIntentNotifications = emitter => {
  const RNEmitter = new NativeEventEmitter(NativeModules.KeybaseEngine)
  // If android launched due to push
  RNEmitter.addListener('androidIntentNotification', (evt) => {
    logger.info('[PushAndroidIntent]', evt && evt.type)
    if (!evt) {
      return
    }
    const notification = Constants.normalizePush(evt)
    if (!notification) {
      return
    }

    emitter(PushGen.createNotification({notification}))
  })

  // TODO: move this out of this file.
  RNEmitter.addListener('onShareData', evt => {
    logger.info('[ShareDataIntent]', evt)
    emitter(RouteTreeGen.createNavigateTo({path: FsConstants.fsRootRouteForNav1}))
    emitter(FsGen.createSetIncomingShareLocalPath({localPath: FsTypes.stringToLocalPath(evt.path)}))
    emitter(FsGen.createShowIncomingShare({initialDestinationParentPath: FsTypes.stringToPath('/keybase')}))
  })
  RNEmitter.addListener('onShareText', evt => {
    logger.info('[ShareTextIntent]', evt)
    emitter(RouteTreeGen.createNavigateTo({path: FsConstants.fsRootRouteForNav1}))
    // TODO: implement
  })
}

const listenForPushNotificationsFromJS = emitter => {
  const onRegister = token => {
    console.log('[PushToken] received new token: ', token)
    emitter(PushGen.createUpdatePushToken({token: token.token}))
  }

  const onNotification = n => {
    if (isAndroid && n.type === 'chat.newmessageSilent_2') {
      const convID = n.c
      const payload = n.m
      const membersType = parseInt(n.t)
      const displayPlaintext = n.n === 'true'
      const messageId = parseInt(n.d)
      const pushId = JSON.parse(n.p)[0]
      const badgeCount = parseInt(n.b)
      const unixTime = parseInt(n.x)
      const soundName = n.s
      NativeModules.PushHandler.handlePushNotification(
        convID,
        payload,
        membersType,
        displayPlaintext,
        messageId,
        pushId,
        badgeCount,
        unixTime,
        soundName,
      )
      return
    }
    const notification = Constants.normalizePush(n)
    if (!notification) {
      return
    }
    emitter(PushGen.createNotification({notification}))
  }

  const onError = error => {
    logger.error('push error:', error)
  }

  PushNotifications.configure({
    onError,
    onNotification,
    onRegister,
    popInitialNotification: false,
    // Don't request permissions for ios, we'll ask later, after showing UI
    requestPermissions: !isIOS,
    senderID: Constants.androidSenderID,
  })
}

function* setupPushEventLoop() {
  const pushChannel = yield Saga.eventChannel(emitter => {
    if (!isIOS) {
      listenForNativeAndroidIntentNotifications(emitter)
    }
    listenForPushNotificationsFromJS(emitter)

    // we never unsubscribe
    return () => {}
  }, Saga.buffers.expanding(10))

  while (true) {
    const action = yield Saga.take(pushChannel)
    yield Saga.put(action)
  }
}

function* handleLoudMessage(notification) {
  // We only care if the user clicked while in session
  if (!notification.userInteraction) {
    return
  }

  const {conversationIDKey, unboxPayload, membersType} = notification

  yield Saga.put(Chat2Gen.createSelectConversation({conversationIDKey, reason: 'push'}))
  yield Saga.put(Chat2Gen.createNavigateToThread())
  if (unboxPayload && membersType && !isIOS) {
    logger.info('[Push] unboxing message')
    try {
      yield* Saga.callPromise(RPCChatTypes.localUnboxMobilePushNotificationRpcPromise, {
        convID: conversationIDKey,
        membersType,
        payload: unboxPayload,
        shouldAck: false,
      })
    } catch (e) {
      logger.info('[Push] failed to unbox message form payload')
    }
  }
}

// on iOS the go side handles a lot of push details
function* handlePush(_, action) {
  try {
    const notification = action.payload.notification
    logger.info('[Push]: ' + notification.type || 'unknown')

    switch (notification.type) {
      case 'chat.readmessage':
        logger.info('[Push] read message')
        if (notification.badges === 0) {
          PushNotifications.cancelAllLocalNotifications()
        }
        break
      case 'chat.newmessageSilent_2':
        // entirely handled by go on ios and in onNotification on Android
        break
      case 'chat.newmessage':
        yield* handleLoudMessage(notification)
        break
      case 'follow':
        // We only care if the user clicked while in session
        if (notification.userInteraction) {
          const {username} = notification
          logger.info('[Push] follower: ', username)
          yield Saga.put(ProfileGen.createShowUserProfile({username}))
        }
        break
      case 'chat.extension':
        const {conversationIDKey} = notification
        yield Saga.put(Chat2Gen.createSelectConversation({conversationIDKey, reason: 'extension'}))
        break
    }
  } catch (e) {
    if (__DEV__) {
      console.error(e)
    }

    logger.error('[Push] unhandled!!')
  }
}

const uploadPushToken = state =>
  !!state.push.token &&
  !!state.config.deviceID &&
  RPCTypes.apiserverPostRpcPromise({
    args: [
      {key: 'push_token', value: state.push.token},
      {key: 'device_id', value: state.config.deviceID},
      {key: 'token_type', value: Constants.tokenType},
    ],
    endpoint: 'device/push_token',
  })
    .then(() => {
      logger.info('[PushToken] Uploaded to server')
      return false
    })
    .catch(e => {
      logger.error("[PushToken] Couldn't save a push token", e)
    })

function* deletePushToken(state, action) {
  const waitKey = 'push:deleteToken'
  yield Saga.put(
    ConfigGen.createLogoutHandshakeWait({increment: true, name: waitKey, version: action.payload.version})
  )

  try {
    const deviceID = state.config.deviceID
    if (!deviceID) {
      logger.info('[PushToken] no device id')
      return
    }

    yield* Saga.callPromise(RPCTypes.apiserverDeleteRpcPromise, {
      args: [{key: 'device_id', value: deviceID}, {key: 'token_type', value: Constants.tokenType}],
      endpoint: 'device/push_token',
    })
    logger.info('[PushToken] deleted from server')
  } catch (e) {
    logger.error('[PushToken] delete failed', e)
  } finally {
    yield Saga.put(
      ConfigGen.createLogoutHandshakeWait({
        increment: false,
        name: waitKey,
        version: action.payload.version,
      })
    )
  }
}

const requestPermissionsFromNative = () =>
  isIOS ? PushNotifications.requestPermissions() : Promise.resolve()
const askNativeIfSystemPushPromptHasBeenShown = () =>
  isIOS ? NativeModules.PushPrompt.getHasShownPushPrompt() : Promise.resolve(false)
const checkPermissionsFromNative = () =>
  new Promise((resolve, reject) => PushNotifications.checkPermissions(resolve))
const monsterStorageKey = 'shownMonsterPushPrompt'

function* neverShowMonsterAgain(state) {
  if (!state.push.showPushPrompt) {
    yield Saga.spawn(() =>
      RPCTypes.configSetValueRpcPromise({path: `ui.${monsterStorageKey}`, value: {b: true, isNull: false}})
    )
  }
}

function* requestPermissions() {
  if (isIOS) {
    const shownPushPrompt = yield* Saga.callPromise(askNativeIfSystemPushPromptHasBeenShown)
    if (shownPushPrompt) {
      // we've already shown the prompt, take them to settings
      yield Saga.put(ConfigGen.createOpenAppSettings())
      yield Saga.put(PushGen.createShowPermissionsPrompt({show: false}))
      return
    }
  }
  try {
    yield Saga.put(WaitingGen.createIncrementWaiting({key: Constants.permissionsRequestingWaitingKey}))
    logger.info('[PushRequesting] asking native')
    const permissions = yield* Saga.callPromise(requestPermissionsFromNative)
    logger.info('[PushRequesting] after prompt:', permissions)
    if (permissions?.alert || permissions?.badge) {
      logger.info('[PushRequesting] enabled')
      yield Saga.put(PushGen.createUpdateHasPermissions({hasPermissions: true}))
    } else {
      logger.info('[PushRequesting] disabled')
      yield Saga.put(PushGen.createUpdateHasPermissions({hasPermissions: false}))
    }
  } finally {
    yield Saga.put(WaitingGen.createDecrementWaiting({key: Constants.permissionsRequestingWaitingKey}))
    yield Saga.put(PushGen.createShowPermissionsPrompt({show: false}))
  }
}

function* initialPermissionsCheck(): Saga.SagaGenerator<any, any> {
  const hasPermissions = yield _checkPermissions(null)
  if (hasPermissions) {
    // Get the token
    yield Saga.spawn(requestPermissionsFromNative)
  } else {
    const shownNativePushPromptTask = yield Saga._fork(askNativeIfSystemPushPromptHasBeenShown)
    const shownMonsterPushPromptTask = yield Saga._fork(() =>
      RPCTypes.configGetValueRpcPromise({path: `ui.${monsterStorageKey}`})
        .then(v => !!v.b)
        .catch(() => false)
    )
    const [shownNativePushPrompt, shownMonsterPushPrompt] = yield Saga.join(
      shownNativePushPromptTask,
      shownMonsterPushPromptTask
    )
    logger.info(
      '[PushInitialCheck] shownNativePushPrompt:',
      shownNativePushPrompt,
      'shownMonsterPushPrompt:',
      shownMonsterPushPrompt
    )
    if (!shownNativePushPrompt && !shownMonsterPushPrompt) {
      logger.info('[PushInitialCheck] no permissions, never shown prompt, now show prompt')
      yield Saga.put(PushGen.createShowPermissionsPrompt({show: true}))
    }
  }
}

function* checkPermissions(_, action: ConfigGen.MobileAppStatePayload | null) {
  yield* _checkPermissions(action)
}
// Call when we foreground and on app start, action is null on app start. Returns if you have permissions
function* _checkPermissions(action: ConfigGen.MobileAppStatePayload | null) {
  // Only recheck on foreground, not background
  if (action && action.payload.nextAppState !== 'active') {
    logger.info('[PushCheck] skip on backgrounding')
    return false
  }

  console.log('[PushCheck] checking ', action ? 'on foreground' : 'on startup')
  const permissions = yield* Saga.callPromise(checkPermissionsFromNative)
  if (permissions.alert || permissions.badge) {
    const state = yield* Saga.selectState()
    if (!state.push.hasPermissions) {
      logger.info('[PushCheck] enabled: getting token')
      yield Saga.put(PushGen.createUpdateHasPermissions({hasPermissions: true}))
      yield* Saga.callPromise(requestPermissionsFromNative)
    } else {
      logger.info('[PushCheck] enabled already')
    }
    return true
  } else {
    logger.info('[PushCheck] disabled')
    yield Saga.put(PushGen.createUpdateHasPermissions({hasPermissions: false}))
    return false
  }
}

const getStartupDetailsFromInitialPush = (): Promise<
  null | {startupFollowUser: string} | {startupConversation: ChatTypes.ConversationIDKey}
> =>
  new Promise(resolve => {
    PushNotifications.popInitialNotification(n => {
      const notification = Constants.normalizePush(n)
      if (!notification) {
        resolve(null)
        return
      }
      if (notification.type === 'follow') {
        if (notification.username) {
          resolve({startupFollowUser: notification.username})
          return
        }
      } else if (notification.type === 'chat.newmessage') {
        if (notification.conversationIDKey) {
          resolve({startupConversation: notification.conversationIDKey})
          return
        }
      }
      resolve(null)
    })
  })

function* pushSaga(): Saga.SagaGenerator<any, any> {
  // Permissions
  yield* Saga.chainGenerator<PushGen.RequestPermissionsPayload>(
    PushGen.requestPermissions,
    requestPermissions
  )
  yield* Saga.chainGenerator<PushGen.ShowPermissionsPromptPayload | PushGen.RejectPermissionsPayload>(
    [PushGen.showPermissionsPrompt, PushGen.rejectPermissions],
    neverShowMonsterAgain
  )
  yield* Saga.chainGenerator<ConfigGen.MobileAppStatePayload>(ConfigGen.mobileAppState, checkPermissions)

  // Token handling
  yield* Saga.chainAction<PushGen.UpdatePushTokenPayload | ConfigGen.BootstrapStatusLoadedPayload>(
    [PushGen.updatePushToken, ConfigGen.bootstrapStatusLoaded],
    uploadPushToken
  )
  yield* Saga.chainGenerator<ConfigGen.LogoutHandshakePayload>(ConfigGen.logoutHandshake, deletePushToken)

  yield* Saga.chainAction<NotificationsGen.ReceivedBadgeStatePayload>(
    NotificationsGen.receivedBadgeState,
    updateAppBadge
  )
  yield* Saga.chainGenerator<PushGen.NotificationPayload>(PushGen.notification, handlePush)
  yield* Saga.chainGenerator<ConfigGen.DaemonHandshakePayload>(ConfigGen.daemonHandshake, setupPushEventLoop)
  yield Saga.spawn(initialPermissionsCheck)
}

export default pushSaga
export {getStartupDetailsFromInitialPush}
<|endoftext|>