<commit_before>'use strict';
// @ts-check

const fs = require('fs');
const helper = require('./lib/chromedriver');
const axios = require('axios').default;
const path = require('path');
const del = require('del');
const child_process = require('child_process');
const os = require('os');
const url = require('url');
const https = require('https');
const extractZip = require('extract-zip');
const { getChromeVersion } = require('@testim/chrome-version');
const HttpsProxyAgent = require('https-proxy-agent');
const getProxyForUrl = require("proxy-from-env").getProxyForUrl;

const skipDownload = process.env.npm_config_chromedriver_skip_download || process.env.CHROMEDRIVER_SKIP_DOWNLOAD;
if (skipDownload === 'true') {
  console.log('Found CHROMEDRIVER_SKIP_DOWNLOAD variable, skipping installation.');
  process.exit(0);
}

const libPath = path.join(__dirname, 'lib', 'chromedriver');
let cdnUrl = process.env.npm_config_chromedriver_cdnurl || process.env.CHROMEDRIVER_CDNURL || 'https://chromedriver.storage.googleapis.com';
const configuredfilePath = process.env.npm_config_chromedriver_filepath || process.env.CHROMEDRIVER_FILEPATH;

// adapt http://chromedriver.storage.googleapis.com/
cdnUrl = cdnUrl.replace(/\/+$/, '');
const platform = validatePlatform();
const detect_chromedriver_version = process.env.npm_config_detect_chromedriver_version || process.env.DETECT_CHROMEDRIVER_VERSION;
let chromedriver_version = process.env.npm_config_chromedriver_version || process.env.CHROMEDRIVER_VERSION || helper.version;
let chromedriverBinaryFilePath;
let downloadedFile = '';

(async function install() {
  try {
    if (detect_chromedriver_version === 'true') {
      // Refer http://chromedriver.chromium.org/downloads/version-selection
      const chromeVersion = await getChromeVersion();
      console.log("Your Chrome version is " + chromeVersion);
      const chromeVersionWithoutPatch = /^(.*?)\.\d+$/.exec(chromeVersion)[1];
      await getChromeDriverVersion(getRequestOptions(cdnUrl + '/LATEST_RELEASE_' + chromeVersionWithoutPatch));
      console.log("Compatible ChromeDriver version is " + chromedriver_version);
    }
    if (chromedriver_version === 'LATEST') {
      await getChromeDriverVersion(getRequestOptions(`${cdnUrl}/LATEST_RELEASE`));
    } else {
      const latestReleaseForVersionMatch = chromedriver_version.match(/LATEST_(\d+)/);
      if (latestReleaseForVersionMatch) {
        const majorVersion = latestReleaseForVersionMatch[1];
        await getChromeDriverVersion(getRequestOptions(`${cdnUrl}/LATEST_RELEASE_${majorVersion}`));
      }
    }
    const tmpPath = findSuitableTempDirectory();
    const chromedriverBinaryFileName = process.platform === 'win32' ? 'chromedriver.exe' : 'chromedriver';
    chromedriverBinaryFilePath = path.resolve(tmpPath, chromedriverBinaryFileName);
    const chromedriverIsAvailable = await verifyIfChromedriverIsAvailableAndHasCorrectVersion();
    if (!chromedriverIsAvailable) {
      console.log('Current existing ChromeDriver binary is unavailable, proceeding with download and extraction.');
      await downloadFile(tmpPath);
      await extractDownload(tmpPath);
    }
    await copyIntoPlace(tmpPath, libPath);
    fixFilePermissions();
    console.log('Done. ChromeDriver binary available at', helper.path);
  } catch (err) {
    console.error('ChromeDriver installation failed', err);
    process.exit(1);
  }
})();

function validatePlatform() {
  /** @type string */
  let thePlatform = process.platform;
  if (thePlatform === 'linux') {
    if (process.arch === 'arm64' || process.arch === 'x64') {
      thePlatform += '64';
    } else {
      console.log('Only Linux 64 bits supported.');
      process.exit(1);
    }
  } else if (thePlatform === 'darwin' || thePlatform === 'freebsd') {
    if (process.arch === 'x64' || process.arch === 'arm64') {
      thePlatform = 'mac64';
    } else {
      console.log('Only Mac 64 bits supported.');
      process.exit(1);
    }
  } else if (thePlatform !== 'win32') {
    console.log('Unexpected platform or architecture:', process.platform, process.arch);
    process.exit(1);
  }
  return thePlatform;
}

async function downloadFile(dirToLoadTo) {
  if (detect_chromedriver_version !== 'true' && configuredfilePath) {
    downloadedFile = configuredfilePath;
    console.log('Using file: ', downloadedFile);
    return;
  } else {
    const fileName = `chromedriver_${platform}.zip`;
    const tempDownloadedFile = path.resolve(dirToLoadTo, fileName);
    downloadedFile = tempDownloadedFile;
    const formattedDownloadUrl = `${cdnUrl}/${chromedriver_version}/${fileName}`;
    console.log('Downloading from file: ', formattedDownloadUrl);
    console.log('Saving to file:', downloadedFile);
    await requestBinary(getRequestOptions(formattedDownloadUrl), downloadedFile);
  }
}

function verifyIfChromedriverIsAvailableAndHasCorrectVersion() {
  if (!fs.existsSync(chromedriverBinaryFilePath))
    return Promise.resolve(false);
  const forceDownload = process.env.npm_config_chromedriver_force_download === 'true' || process.env.CHROMEDRIVER_FORCE_DOWNLOAD === 'true';
  if (forceDownload)
    return Promise.resolve(false);
  console.log('ChromeDriver binary exists. Validating...');
  const deferred = new Deferred();
  try {
    fs.accessSync(chromedriverBinaryFilePath, fs.constants.X_OK);
    const cp = child_process.spawn(chromedriverBinaryFilePath, ['--version']);
    let str = '';
    cp.stdout.on('data', data => str += data);
    cp.on('error', () => deferred.resolve(false));
    cp.on('close', code => {
      if (code !== 0)
        return deferred.resolve(false);
      const parts = str.split(' ');
      if (parts.length < 3)
        return deferred.resolve(false);
      if (parts[1].startsWith(chromedriver_version)) {
        console.log(`ChromeDriver is already available at '${chromedriverBinaryFilePath}'.`);
        return deferred.resolve(true);
      }
      deferred.resolve(false);
    });
  }
  catch (error) {
    deferred.resolve(false);
  }
  return deferred.promise;
}

function findSuitableTempDirectory() {
  const now = Date.now();
  const candidateTmpDirs = [
    process.env.npm_config_tmp,
    process.env.XDG_CACHE_HOME,
    // Platform specific default, including TMPDIR/TMP/TEMP env
    os.tmpdir(),
    path.join(process.cwd(), 'tmp')
  ];

  for (let i = 0; i < candidateTmpDirs.length; i++) {
    if (!candidateTmpDirs[i]) continue;
    // Prevent collision with other versions in the dependency tree
    const namespace = chromedriver_version;
    const candidatePath = path.join(candidateTmpDirs[i], namespace, 'chromedriver');
    try {
      fs.mkdirSync(candidatePath, { recursive: true });
      const testFile = path.join(candidatePath, now + '.tmp');
      fs.writeFileSync(testFile, 'test');
      fs.unlinkSync(testFile);
      return candidatePath;
    } catch (e) {
      console.log(candidatePath, 'is not writable:', e.message);
    }
  }
  console.error('Can not find a writable tmp directory, please report issue on https://github.com/giggio/chromedriver/issues/ with as much information as possible.');
  process.exit(1);
}

function getRequestOptions(downloadPath) {
  /** @type import('axios').AxiosRequestConfig */
  const options = { url: downloadPath, method: "GET" };
  const urlParts = url.parse(downloadPath);
  const isHttps = urlParts.protocol === 'https:';
  const proxyUrl = getProxyForUrl(downloadPath);

  if (proxyUrl) {
    const proxyUrlParts = url.parse(proxyUrl);
    options.proxy = {
      host: proxyUrlParts.hostname,
      port: proxyUrlParts.port ? parseInt(proxyUrlParts.port) : 80,
      protocol: proxyUrlParts.protocol
    };
  }

  if (isHttps) {
    // Use certificate authority settings from npm
    let ca = process.env.npm_config_ca;
    if (ca)
      console.log('Using npmconf ca.');

    if (!ca && process.env.npm_config_cafile) {
      try {
        ca = fs.readFileSync(process.env.npm_config_cafile, { encoding: 'utf8' });
      } catch (e) {
        console.error('Could not read cafile', process.env.npm_config_cafile, e);
      }
      console.log('Using npmconf cafile.');
    }

    if (proxyUrl) {
      console.log('Using workaround for https-url combined with a proxy.');
      const httpsProxyAgentOptions = url.parse(proxyUrl);
      // @ts-ignore
      httpsProxyAgentOptions.ca = ca;
      // @ts-ignore
      httpsProxyAgentOptions.rejectUnauthorized = !!process.env.npm_config_strict_ssl;
      // @ts-ignore
      options.httpsAgent = new HttpsProxyAgent(httpsProxyAgentOptions);
      options.proxy = false;
    } else {
      options.httpsAgent = new https.Agent({
        rejectUnauthorized: !!process.env.npm_config_strict_ssl,
        ca: ca
      });
    }
  }

  // Use specific User-Agent
  if (process.env.npm_config_user_agent) {
    options.headers = { 'User-Agent': process.env.npm_config_user_agent };
  }

  return options;
}

/**
 *
 * @param {import('axios').AxiosRequestConfig} requestOptions
 */
async function getChromeDriverVersion(requestOptions) {
  console.log('Finding Chromedriver version.');
  const response = await axios(requestOptions);
  chromedriver_version = response.data.trim();
  console.log(`Chromedriver version is ${chromedriver_version}.`);
}

/**
 *
 * @param {import('axios').AxiosRequestConfig} requestOptions
 * @param {string} filePath
 */
async function requestBinary(requestOptions, filePath) {
  const outFile = fs.createWriteStream(filePath);
  let response;
  try {
    response = await axios({ responseType: 'stream', ...requestOptions });
  } catch (error) {
    if (error && error.response) {
      if (error.response.status)
        console.error('Error status code:', error.response.status);
      if (error.response.data) {
        error.response.data.on('data', data => console.error(data.toString('utf8')));
        await new Promise((resolve) => {
          error.response.data.on('finish', resolve);
          error.response.data.on('error', resolve);
        });
      }
    }
    throw new Error('Error with http(s) request: ' + error);
  }
  let count = 0;
  let notifiedCount = 0;
  response.data.on('data', data => {
    count += data.length;
    if ((count - notifiedCount) > 1024 * 1024) {
      console.log('Received ' + Math.floor(count / 1024) + 'K...');
      notifiedCount = count;
    }
  });
  response.data.on('end', () => console.log('Received ' + Math.floor(count / 1024) + 'K total.'));
  const pipe = response.data.pipe(outFile);
  await new Promise((resolve, reject) => {
    pipe.on('finish', resolve);
    pipe.on('error', reject);
  });
}

async function extractDownload(dirToExtractTo) {
  if (path.extname(downloadedFile) !== '.zip') {
    fs.copyFileSync(downloadedFile, chromedriverBinaryFilePath);
    console.log('Skipping zip extraction - binary file found.');
    return;
  }
  console.log(`Extracting zip contents to ${dirToExtractTo}.`);
  try {
    await extractZip(path.resolve(downloadedFile), { dir: dirToExtractTo });
  } catch (error) {
    throw new Error('Error extracting archive: ' + error);
  }
}

async function copyIntoPlace(originPath, targetPath) {
  await del(targetPath, { force: true });
  console.log(`Copying from ${originPath} to target path ${targetPath}`);
  fs.mkdirSync(targetPath);

  // Look for the extracted directory, so we can rename it.
  const files = fs.readdirSync(originPath, { withFileTypes: true })
    .filter(dirent => dirent.isFile() && dirent.name.startsWith('chromedriver') && !dirent.name.endsWith(".debug") && !dirent.name.endsWith(".zip"))
    .map(dirent => dirent.name);
  const promises = files.map(name => {
    return new Promise((resolve) => {
      const file = path.join(originPath, name);
      const reader = fs.createReadStream(file);
      const targetFile = path.join(targetPath, name);
      const writer = fs.createWriteStream(targetFile);
      writer.on("close", () => resolve());
      reader.pipe(writer);
    });
  });
  await Promise.all(promises);
}

function fixFilePermissions() {
  // Check that the binary is user-executable and fix it if it isn't (problems with unzip library)
  if (process.platform != 'win32') {
    const stat = fs.statSync(helper.path);
    // 64 == 0100 (no octal literal in strict mode)
    if (!(stat.mode & 64)) {
      console.log('Fixing file permissions.');
      fs.chmodSync(helper.path, '755');
    }
  }
}

function Deferred() {
  this.resolve = null;
  this.reject = null;
  this.promise = new Promise(function (resolve, reject) {
    this.resolve = resolve;
    this.reject = reject;
  }.bind(this));
  Object.freeze(this);
}
<commit_msg>[install] properly handle error status codes (#338)<commit_after>'use strict';
// @ts-check

const fs = require('fs');
const helper = require('./lib/chromedriver');
const axios = require('axios').default;
const path = require('path');
const del = require('del');
const child_process = require('child_process');
const os = require('os');
const url = require('url');
const https = require('https');
const { promisify } = require('util');
const { finished } = require('stream');
const extractZip = require('extract-zip');
const { getChromeVersion } = require('@testim/chrome-version');
const HttpsProxyAgent = require('https-proxy-agent');
const getProxyForUrl = require("proxy-from-env").getProxyForUrl;

const finishedAsync = promisify(finished);

const skipDownload = process.env.npm_config_chromedriver_skip_download || process.env.CHROMEDRIVER_SKIP_DOWNLOAD;
if (skipDownload === 'true') {
  console.log('Found CHROMEDRIVER_SKIP_DOWNLOAD variable, skipping installation.');
  process.exit(0);
}

const libPath = path.join(__dirname, 'lib', 'chromedriver');
let cdnUrl = process.env.npm_config_chromedriver_cdnurl || process.env.CHROMEDRIVER_CDNURL || 'https://chromedriver.storage.googleapis.com';
const configuredfilePath = process.env.npm_config_chromedriver_filepath || process.env.CHROMEDRIVER_FILEPATH;

// adapt http://chromedriver.storage.googleapis.com/
cdnUrl = cdnUrl.replace(/\/+$/, '');
const platform = validatePlatform();
const detect_chromedriver_version = process.env.npm_config_detect_chromedriver_version || process.env.DETECT_CHROMEDRIVER_VERSION;
let chromedriver_version = process.env.npm_config_chromedriver_version || process.env.CHROMEDRIVER_VERSION || helper.version;
let chromedriverBinaryFilePath;
let downloadedFile = '';

(async function install() {
  try {
    if (detect_chromedriver_version === 'true') {
      // Refer http://chromedriver.chromium.org/downloads/version-selection
      const chromeVersion = await getChromeVersion();
      console.log("Your Chrome version is " + chromeVersion);
      const chromeVersionWithoutPatch = /^(.*?)\.\d+$/.exec(chromeVersion)[1];
      await getChromeDriverVersion(getRequestOptions(cdnUrl + '/LATEST_RELEASE_' + chromeVersionWithoutPatch));
      console.log("Compatible ChromeDriver version is " + chromedriver_version);
    }
    if (chromedriver_version === 'LATEST') {
      await getChromeDriverVersion(getRequestOptions(`${cdnUrl}/LATEST_RELEASE`));
    } else {
      const latestReleaseForVersionMatch = chromedriver_version.match(/LATEST_(\d+)/);
      if (latestReleaseForVersionMatch) {
        const majorVersion = latestReleaseForVersionMatch[1];
        await getChromeDriverVersion(getRequestOptions(`${cdnUrl}/LATEST_RELEASE_${majorVersion}`));
      }
    }
    const tmpPath = findSuitableTempDirectory();
    const chromedriverBinaryFileName = process.platform === 'win32' ? 'chromedriver.exe' : 'chromedriver';
    chromedriverBinaryFilePath = path.resolve(tmpPath, chromedriverBinaryFileName);
    const chromedriverIsAvailable = await verifyIfChromedriverIsAvailableAndHasCorrectVersion();
    if (!chromedriverIsAvailable) {
      console.log('Current existing ChromeDriver binary is unavailable, proceeding with download and extraction.');
      await downloadFile(tmpPath);
      await extractDownload(tmpPath);
    }
    await copyIntoPlace(tmpPath, libPath);
    fixFilePermissions();
    console.log('Done. ChromeDriver binary available at', helper.path);
  } catch (err) {
    console.error('ChromeDriver installation failed', err);
    process.exit(1);
  }
})();

function validatePlatform() {
  /** @type string */
  let thePlatform = process.platform;
  if (thePlatform === 'linux') {
    if (process.arch === 'arm64' || process.arch === 'x64') {
      thePlatform += '64';
    } else {
      console.log('Only Linux 64 bits supported.');
      process.exit(1);
    }
  } else if (thePlatform === 'darwin' || thePlatform === 'freebsd') {
    if (process.arch === 'x64' || process.arch === 'arm64') {
      thePlatform = 'mac64';
    } else {
      console.log('Only Mac 64 bits supported.');
      process.exit(1);
    }
  } else if (thePlatform !== 'win32') {
    console.log('Unexpected platform or architecture:', process.platform, process.arch);
    process.exit(1);
  }
  return thePlatform;
}

async function downloadFile(dirToLoadTo) {
  if (detect_chromedriver_version !== 'true' && configuredfilePath) {
    downloadedFile = configuredfilePath;
    console.log('Using file: ', downloadedFile);
    return;
  } else {
    const fileName = `chromedriver_${platform}.zip`;
    const tempDownloadedFile = path.resolve(dirToLoadTo, fileName);
    downloadedFile = tempDownloadedFile;
    const formattedDownloadUrl = `${cdnUrl}/${chromedriver_version}/${fileName}`;
    console.log('Downloading from file: ', formattedDownloadUrl);
    console.log('Saving to file:', downloadedFile);
    await requestBinary(getRequestOptions(formattedDownloadUrl), downloadedFile);
  }
}

function verifyIfChromedriverIsAvailableAndHasCorrectVersion() {
  if (!fs.existsSync(chromedriverBinaryFilePath))
    return Promise.resolve(false);
  const forceDownload = process.env.npm_config_chromedriver_force_download === 'true' || process.env.CHROMEDRIVER_FORCE_DOWNLOAD === 'true';
  if (forceDownload)
    return Promise.resolve(false);
  console.log('ChromeDriver binary exists. Validating...');
  const deferred = new Deferred();
  try {
    fs.accessSync(chromedriverBinaryFilePath, fs.constants.X_OK);
    const cp = child_process.spawn(chromedriverBinaryFilePath, ['--version']);
    let str = '';
    cp.stdout.on('data', data => str += data);
    cp.on('error', () => deferred.resolve(false));
    cp.on('close', code => {
      if (code !== 0)
        return deferred.resolve(false);
      const parts = str.split(' ');
      if (parts.length < 3)
        return deferred.resolve(false);
      if (parts[1].startsWith(chromedriver_version)) {
        console.log(`ChromeDriver is already available at '${chromedriverBinaryFilePath}'.`);
        return deferred.resolve(true);
      }
      deferred.resolve(false);
    });
  }
  catch (error) {
    deferred.resolve(false);
  }
  return deferred.promise;
}

function findSuitableTempDirectory() {
  const now = Date.now();
  const candidateTmpDirs = [
    process.env.npm_config_tmp,
    process.env.XDG_CACHE_HOME,
    // Platform specific default, including TMPDIR/TMP/TEMP env
    os.tmpdir(),
    path.join(process.cwd(), 'tmp')
  ];

  for (let i = 0; i < candidateTmpDirs.length; i++) {
    if (!candidateTmpDirs[i]) continue;
    // Prevent collision with other versions in the dependency tree
    const namespace = chromedriver_version;
    const candidatePath = path.join(candidateTmpDirs[i], namespace, 'chromedriver');
    try {
      fs.mkdirSync(candidatePath, { recursive: true });
      const testFile = path.join(candidatePath, now + '.tmp');
      fs.writeFileSync(testFile, 'test');
      fs.unlinkSync(testFile);
      return candidatePath;
    } catch (e) {
      console.log(candidatePath, 'is not writable:', e.message);
    }
  }
  console.error('Can not find a writable tmp directory, please report issue on https://github.com/giggio/chromedriver/issues/ with as much information as possible.');
  process.exit(1);
}

function getRequestOptions(downloadPath) {
  /** @type import('axios').AxiosRequestConfig */
  const options = { url: downloadPath, method: "GET" };
  const urlParts = url.parse(downloadPath);
  const isHttps = urlParts.protocol === 'https:';
  const proxyUrl = getProxyForUrl(downloadPath);

  if (proxyUrl) {
    const proxyUrlParts = url.parse(proxyUrl);
    options.proxy = {
      host: proxyUrlParts.hostname,
      port: proxyUrlParts.port ? parseInt(proxyUrlParts.port) : 80,
      protocol: proxyUrlParts.protocol
    };
  }

  if (isHttps) {
    // Use certificate authority settings from npm
    let ca = process.env.npm_config_ca;
    if (ca)
      console.log('Using npmconf ca.');

    if (!ca && process.env.npm_config_cafile) {
      try {
        ca = fs.readFileSync(process.env.npm_config_cafile, { encoding: 'utf8' });
      } catch (e) {
        console.error('Could not read cafile', process.env.npm_config_cafile, e);
      }
      console.log('Using npmconf cafile.');
    }

    if (proxyUrl) {
      console.log('Using workaround for https-url combined with a proxy.');
      const httpsProxyAgentOptions = url.parse(proxyUrl);
      // @ts-ignore
      httpsProxyAgentOptions.ca = ca;
      // @ts-ignore
      httpsProxyAgentOptions.rejectUnauthorized = !!process.env.npm_config_strict_ssl;
      // @ts-ignore
      options.httpsAgent = new HttpsProxyAgent(httpsProxyAgentOptions);
      options.proxy = false;
    } else {
      options.httpsAgent = new https.Agent({
        rejectUnauthorized: !!process.env.npm_config_strict_ssl,
        ca: ca
      });
    }
  }

  // Use specific User-Agent
  if (process.env.npm_config_user_agent) {
    options.headers = { 'User-Agent': process.env.npm_config_user_agent };
  }

  return options;
}

/**
 *
 * @param {import('axios').AxiosRequestConfig} requestOptions
 */
async function getChromeDriverVersion(requestOptions) {
  console.log('Finding Chromedriver version.');
  const response = await axios(requestOptions);
  chromedriver_version = response.data.trim();
  console.log(`Chromedriver version is ${chromedriver_version}.`);
}

/**
 *
 * @param {import('axios').AxiosRequestConfig} requestOptions
 * @param {string} filePath
 */
async function requestBinary(requestOptions, filePath) {
  const outFile = fs.createWriteStream(filePath);
  let response;
  try {
    response = await axios({ responseType: 'stream', ...requestOptions });
  } catch (error) {
    if (error && error.response) {
      if (error.response.status)
        console.error('Error status code:', error.response.status);
      if (error.response.data) {
        error.response.data.on('data', data => console.error(data.toString('utf8')));
        try {
          await finishedAsync(error.response.data)
        } catch (error) {
          console.error('Error downloading entire response:', error);
        }
      }
    }
    throw new Error('Error with http(s) request: ' + error);
  }
  let count = 0;
  let notifiedCount = 0;
  response.data.on('data', data => {
    count += data.length;
    if ((count - notifiedCount) > 1024 * 1024) {
      console.log('Received ' + Math.floor(count / 1024) + 'K...');
      notifiedCount = count;
    }
  });
  response.data.on('end', () => console.log('Received ' + Math.floor(count / 1024) + 'K total.'));
  const pipe = response.data.pipe(outFile);
  await new Promise((resolve, reject) => {
    pipe.on('finish', resolve);
    pipe.on('error', reject);
  });
}

async function extractDownload(dirToExtractTo) {
  if (path.extname(downloadedFile) !== '.zip') {
    fs.copyFileSync(downloadedFile, chromedriverBinaryFilePath);
    console.log('Skipping zip extraction - binary file found.');
    return;
  }
  console.log(`Extracting zip contents to ${dirToExtractTo}.`);
  try {
    await extractZip(path.resolve(downloadedFile), { dir: dirToExtractTo });
  } catch (error) {
    throw new Error('Error extracting archive: ' + error);
  }
}

async function copyIntoPlace(originPath, targetPath) {
  await del(targetPath, { force: true });
  console.log(`Copying from ${originPath} to target path ${targetPath}`);
  fs.mkdirSync(targetPath);

  // Look for the extracted directory, so we can rename it.
  const files = fs.readdirSync(originPath, { withFileTypes: true })
    .filter(dirent => dirent.isFile() && dirent.name.startsWith('chromedriver') && !dirent.name.endsWith(".debug") && !dirent.name.endsWith(".zip"))
    .map(dirent => dirent.name);
  const promises = files.map(name => {
    return new Promise((resolve) => {
      const file = path.join(originPath, name);
      const reader = fs.createReadStream(file);
      const targetFile = path.join(targetPath, name);
      const writer = fs.createWriteStream(targetFile);
      writer.on("close", () => resolve());
      reader.pipe(writer);
    });
  });
  await Promise.all(promises);
}

function fixFilePermissions() {
  // Check that the binary is user-executable and fix it if it isn't (problems with unzip library)
  if (process.platform != 'win32') {
    const stat = fs.statSync(helper.path);
    // 64 == 0100 (no octal literal in strict mode)
    if (!(stat.mode & 64)) {
      console.log('Fixing file permissions.');
      fs.chmodSync(helper.path, '755');
    }
  }
}

function Deferred() {
  this.resolve = null;
  this.reject = null;
  this.promise = new Promise(function (resolve, reject) {
    this.resolve = resolve;
    this.reject = reject;
  }.bind(this));
  Object.freeze(this);
}
<|endoftext|>


<commit_before>import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { timeSince } from 'helpers/timeStampHelpers';
import { Color } from 'constants/css';
import ContentLink from 'components/ContentLink';
import UsernameText from 'components/Texts/UsernameText';
import RoundList from 'components/RoundList';
import Banner from 'components/Banner';
import LoadMoreButton from 'components/Buttons/LoadMoreButton';
import {
  clearRewards,
  fetchNotifications,
  loadMoreNotifications,
  loadMoreRewards
} from 'redux/actions/NotiActions';
import { changeUserXP } from 'redux/actions/UserActions';
import { connect } from 'react-redux';
import LeaderBoardTab from './LeaderBoardTab';
import { addCommasToNumber } from 'helpers/stringHelpers';
import { notiFeedListItem } from '../Styles';
import { rewardValue } from 'constants/defaultValues';

class MainFeeds extends Component {
  static propTypes = {
    changeUserXP: PropTypes.func.isRequired,
    fetchNotifications: PropTypes.func.isRequired,
    loadMore: PropTypes.object.isRequired,
    loadMoreNotifications: PropTypes.func.isRequired,
    loadMoreRewards: PropTypes.func.isRequired,
    myId: PropTypes.number,
    numNewNotis: PropTypes.number,
    activeTab: PropTypes.string,
    notifications: PropTypes.arrayOf(
      PropTypes.shape({
        contentId: PropTypes.number,
        commentContent: PropTypes.string,
        discussionId: PropTypes.number,
        discussionTitle: PropTypes.string,
        discussionUploader: PropTypes.number,
        id: PropTypes.number.isRequired,
        reward: PropTypes.object,
        rootCommentUploader: PropTypes.oneOfType([
          PropTypes.number,
          PropTypes.string
        ]),
        rootTitle: PropTypes.string,
        rootType: PropTypes.string.isRequired,
        rootId: PropTypes.number.isRequired,
        timeStamp: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
        type: PropTypes.string.isRequired,
        userId: PropTypes.number.isRequired,
        username: PropTypes.string.isRequired
      })
    ).isRequired,
    rewards: PropTypes.array,
    selectNotiTab: PropTypes.func.isRequired,
    style: PropTypes.object,
    totalRewardAmount: PropTypes.number,
    twinkleXP: PropTypes.number
  };

  state = {
    loading: false,
    originalTotalReward: 0,
    originalTwinkleXP: 0
  };

  render() {
    const {
      myId,
      loadMore,
      activeTab,
      notifications,
      numNewNotis,
      rewards,
      style,
      totalRewardAmount
    } = this.props;
    const { loading } = this.state;
    const { originalTotalReward, originalTwinkleXP } = this.state;
    return (

        <RoundList style={{ marginTop: '0' }}>
          {numNewNotis > 0 && (
            <Banner
              love
              style={{ marginBottom: '1rem' }}
              onClick={this.onNewNotiAlertClick}
            >
              Tap to See {numNewNotis} New Notification
              {numNewNotis > 1 ? 's' : ''}
            </Banner>
          )}
          {activeTab === 'reward' && (
            <Banner
              love={totalRewardAmount > 0}
              success={totalRewardAmount === 0}
              style={{ marginBottom: '1rem' }}
              onClick={totalRewardAmount > 0 ? this.onCollectReward : null}
            >
              {totalRewardAmount > 0 && (
                <>

Tap to collect all your rewards

                    ({totalRewardAmount} Twinkles x {rewardValue.star}{' '}
                    XP/Twinkle ={' '}
                    {addCommasToNumber(totalRewardAmount * rewardValue.star)}{' '}
                    XP)

                </>
              )}
              {totalRewardAmount === 0 && (
                <>

{originalTotalReward * rewardValue.star} XP Collected!

                    Your XP went up from {addCommasToNumber(originalTwinkleXP)}{' '}
                    to{' '}
                    {addCommasToNumber(
                      originalTwinkleXP + originalTotalReward * rewardValue.star
                    )}

                </>
              )}
            </Banner>
          )}
          {activeTab === 'notification' &&
            notifications.map(notification => {
              return (

                  {renderNotificationMessage(notification, myId)}

                    {timeSince(notification.timeStamp)}

              );
            })}
          {activeTab === 'leaderboard' && <LeaderBoardTab myId={myId} />}
          {activeTab === 'reward' &&
            rewards.map(
              ({
                id,
                contentId,
                contentType,
                rewardAmount,
                rewardType,
                rewarderId,
                rewarderUsername,
                timeStamp
              }) => (

                    <UsernameText
                      user={{ id: rewarderId, username: rewarderUsername }}
                      color={Color.blue()}
                    />{' '}
                     1 ? Color.gold() : Color.orange(),
                        fontWeight: 'bold'
                      }}
                    >
                      rewarded you {rewardAmount === 1 ? 'a' : rewardAmount}{' '}
                      {rewardType}
                      {rewardAmount > 1 ? 's' : ''}
                    {' '}
                    for your{' '}
                    <ContentLink
                      style={{ color: Color.green() }}
                      content={{ id: contentId, title: contentType }}
                      type={contentType}
                    />

                  {timeSince(timeStamp)}

              )
            )}
        </RoundList>
        {((activeTab === 'notification' && loadMore.notifications) ||
          (activeTab === 'reward' && loadMore.rewards)) && (
          <LoadMoreButton
            loading={loading}
            style={{ marginTop: '1rem' }}
            info
            filled
            stretch
            onClick={this.onLoadMore}
          />
        )}

    );
  }

  onCollectReward = async() => {
    const {
      changeUserXP,
      clearRewards,
      totalRewardAmount,
      twinkleXP
    } = this.props;
    this.setState({
      originalTotalReward: totalRewardAmount,
      originalTwinkleXP: twinkleXP
    });
    await changeUserXP({
      action: 'collect'
    });
    clearRewards();
  };

  onNewNotiAlertClick = async() => {
    const { selectNotiTab, fetchNotifications } = this.props;
    await fetchNotifications();
    selectNotiTab();
  };

  onLoadMore = async() => {
    const {
      notifications,
      rewards,
      loadMoreNotifications,
      loadMoreRewards,
      activeTab
    } = this.props;
    this.setState({ loading: true });
    if (activeTab === 'notification') {
      await loadMoreNotifications(notifications[notifications.length - 1].id);
    } else {
      await loadMoreRewards(rewards[rewards.length - 1].id);
    }
    this.setState({ loading: false });
  };
}

export default connect(
  state => ({
    numNewNotis: state.NotiReducer.numNewNotis,
    totalRewardAmount: state.NotiReducer.totalRewardAmount,
    twinkleXP: state.UserReducer.twinkleXP
  }),
  {
    changeUserXP,
    clearRewards,
    fetchNotifications,
    loadMoreNotifications,
    loadMoreRewards
  }
)(MainFeeds);

function renderNotificationMessage(notification, myId) {
  const {
    contentId,
    discussionId,
    type,
    reward = {},
    rootType,
    rootTitle,
    rootId,
    userId,
    username,
    commentContent,
    rootCommentUploader,
    discussionTitle,
    discussionUploader
  } = notification;
  let action = '';
  let isReplyNotification =
    commentContent && Number(rootCommentUploader) === myId;
  let isDiscussionAnswerNotification =
    discussionTitle && discussionUploader === myId;
  if (isReplyNotification) {
    action = returnCommentActionText('reply');
  } else if (isDiscussionAnswerNotification) {
    action = returnCommentActionText('comment');
  } else {
    switch (type) {
      case 'reward':
        action = (
          <>
             1 ? Color.gold() : Color.orange()
              }}
            >
              rewarded you{' '}
              {reward.rewardAmount === 1 ? 'a' : reward.rewardAmount}{' '}
              {reward.rewardType}
              {reward.rewardAmount > 1 ? 's' : ''}
            {' '}
            for
          </>
        );
        break;
      case 'like':
        action = 'likes';
        break;
      case 'comment':
        action = returnCommentActionText(
          rootType === 'question' || rootType === 'user' ? rootType : 'comment'
        );
        break;
      case 'discussion':
        action = 'added a discussion to';
        break;
      default:
        break;
    }
  }
  const target = `your ${
    isReplyNotification
      ? 'comment'
      : isDiscussionAnswerNotification
        ? 'discussion topic'
        : rootType === 'user'
          ? 'profile'
          : rootType
  }${rootType === 'user' && !isReplyNotification ? '' : ': '}`;
  let contentTitle = isReplyNotification
    ? commentContent
    : (isDiscussionAnswerNotification ? discussionTitle : rootTitle) || '';
  let title =
    contentTitle.length > 50
      ? contentTitle.substr(0, 50) + '...'
      : contentTitle;
  if (isReplyNotification) title = `"${title}"`;
  const content = {
    title,
    id: isReplyNotification
      ? contentId
      : type === 'discussion'
        ? contentId
        : isDiscussionAnswerNotification
          ? discussionId
          : rootId
  };
  return (

      <UsernameText
        user={{ id: userId, username: username }}
        color={Color.blue()}
      />

      {action} {target}
      {!(rootType === 'user' && !isReplyNotification) && (
        <ContentLink
          content={content}
          type={
            isReplyNotification
              ? 'comment'
              : type === 'discussion' || isDiscussionAnswerNotification
                ? 'discussion'
                : rootType
          }
        />
      )}

  );

  function returnCommentActionText(type) {
    const title =
      type === 'comment'
        ? 'commented on'
        : type === 'reply'
          ? 'replied to'
          : type === 'question'
            ? 'answered'
            : 'left a message on';
    return (
      <ContentLink
        style={{ color: Color.green() }}
        content={{ id: contentId, title }}
        type="comment"
      />
    );
  }
}
<commit_msg>reward notification color change<commit_after>import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { timeSince } from 'helpers/timeStampHelpers';
import { Color } from 'constants/css';
import ContentLink from 'components/ContentLink';
import UsernameText from 'components/Texts/UsernameText';
import RoundList from 'components/RoundList';
import Banner from 'components/Banner';
import LoadMoreButton from 'components/Buttons/LoadMoreButton';
import {
  clearRewards,
  fetchNotifications,
  loadMoreNotifications,
  loadMoreRewards
} from 'redux/actions/NotiActions';
import { changeUserXP } from 'redux/actions/UserActions';
import { connect } from 'react-redux';
import LeaderBoardTab from './LeaderBoardTab';
import { addCommasToNumber } from 'helpers/stringHelpers';
import { notiFeedListItem } from '../Styles';
import { rewardValue } from 'constants/defaultValues';

class MainFeeds extends Component {
  static propTypes = {
    changeUserXP: PropTypes.func.isRequired,
    fetchNotifications: PropTypes.func.isRequired,
    loadMore: PropTypes.object.isRequired,
    loadMoreNotifications: PropTypes.func.isRequired,
    loadMoreRewards: PropTypes.func.isRequired,
    myId: PropTypes.number,
    numNewNotis: PropTypes.number,
    activeTab: PropTypes.string,
    notifications: PropTypes.arrayOf(
      PropTypes.shape({
        contentId: PropTypes.number,
        commentContent: PropTypes.string,
        discussionId: PropTypes.number,
        discussionTitle: PropTypes.string,
        discussionUploader: PropTypes.number,
        id: PropTypes.number.isRequired,
        reward: PropTypes.object,
        rootCommentUploader: PropTypes.oneOfType([
          PropTypes.number,
          PropTypes.string
        ]),
        rootTitle: PropTypes.string,
        rootType: PropTypes.string.isRequired,
        rootId: PropTypes.number.isRequired,
        timeStamp: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
        type: PropTypes.string.isRequired,
        userId: PropTypes.number.isRequired,
        username: PropTypes.string.isRequired
      })
    ).isRequired,
    rewards: PropTypes.array,
    selectNotiTab: PropTypes.func.isRequired,
    style: PropTypes.object,
    totalRewardAmount: PropTypes.number,
    twinkleXP: PropTypes.number
  };

  state = {
    loading: false,
    originalTotalReward: 0,
    originalTwinkleXP: 0
  };

  render() {
    const {
      myId,
      loadMore,
      activeTab,
      notifications,
      numNewNotis,
      rewards,
      style,
      totalRewardAmount
    } = this.props;
    const { loading } = this.state;
    const { originalTotalReward, originalTwinkleXP } = this.state;
    return (

        <RoundList style={{ marginTop: '0' }}>
          {numNewNotis > 0 && (
            <Banner
              love
              style={{ marginBottom: '1rem' }}
              onClick={this.onNewNotiAlertClick}
            >
              Tap to See {numNewNotis} New Notification
              {numNewNotis > 1 ? 's' : ''}
            </Banner>
          )}
          {activeTab === 'reward' && (
            <Banner
              love={totalRewardAmount > 0}
              success={totalRewardAmount === 0}
              style={{ marginBottom: '1rem' }}
              onClick={totalRewardAmount > 0 ? this.onCollectReward : null}
            >
              {totalRewardAmount > 0 && (
                <>

Tap to collect all your rewards

                    ({totalRewardAmount} Twinkles x {rewardValue.star}{' '}
                    XP/Twinkle ={' '}
                    {addCommasToNumber(totalRewardAmount * rewardValue.star)}{' '}
                    XP)

                </>
              )}
              {totalRewardAmount === 0 && (
                <>

{originalTotalReward * rewardValue.star} XP Collected!

                    Your XP went up from {addCommasToNumber(originalTwinkleXP)}{' '}
                    to{' '}
                    {addCommasToNumber(
                      originalTwinkleXP + originalTotalReward * rewardValue.star
                    )}

                </>
              )}
            </Banner>
          )}
          {activeTab === 'notification' &&
            notifications.map(notification => {
              return (

                  {renderNotificationMessage(notification, myId)}

                    {timeSince(notification.timeStamp)}

              );
            })}
          {activeTab === 'leaderboard' && <LeaderBoardTab myId={myId} />}
          {activeTab === 'reward' &&
            rewards.map(
              ({
                id,
                contentId,
                contentType,
                rewardAmount,
                rewardType,
                rewarderId,
                rewarderUsername,
                timeStamp
              }) => (

                    <UsernameText
                      user={{ id: rewarderId, username: rewarderUsername }}
                      color={Color.blue()}
                    />{' '}
                     9
                            ? Color.gold()
                            : rewardAmount > 4
                              ? Color.orange()
                              : Color.lightBlue(),
                        fontWeight: 'bold'
                      }}
                    >
                      rewarded you {rewardAmount === 1 ? 'a' : rewardAmount}{' '}
                      {rewardType}
                      {rewardAmount > 1 ? 's' : ''}
                    {' '}
                    for your{' '}
                    <ContentLink
                      style={{ color: Color.green() }}
                      content={{ id: contentId, title: contentType }}
                      type={contentType}
                    />

                  {timeSince(timeStamp)}

              )
            )}
        </RoundList>
        {((activeTab === 'notification' && loadMore.notifications) ||
          (activeTab === 'reward' && loadMore.rewards)) && (
          <LoadMoreButton
            loading={loading}
            style={{ marginTop: '1rem' }}
            info
            filled
            stretch
            onClick={this.onLoadMore}
          />
        )}

    );
  }

  onCollectReward = async() => {
    const {
      changeUserXP,
      clearRewards,
      totalRewardAmount,
      twinkleXP
    } = this.props;
    this.setState({
      originalTotalReward: totalRewardAmount,
      originalTwinkleXP: twinkleXP
    });
    await changeUserXP({
      action: 'collect'
    });
    clearRewards();
  };

  onNewNotiAlertClick = async() => {
    const { selectNotiTab, fetchNotifications } = this.props;
    await fetchNotifications();
    selectNotiTab();
  };

  onLoadMore = async() => {
    const {
      notifications,
      rewards,
      loadMoreNotifications,
      loadMoreRewards,
      activeTab
    } = this.props;
    this.setState({ loading: true });
    if (activeTab === 'notification') {
      await loadMoreNotifications(notifications[notifications.length - 1].id);
    } else {
      await loadMoreRewards(rewards[rewards.length - 1].id);
    }
    this.setState({ loading: false });
  };
}

export default connect(
  state => ({
    numNewNotis: state.NotiReducer.numNewNotis,
    totalRewardAmount: state.NotiReducer.totalRewardAmount,
    twinkleXP: state.UserReducer.twinkleXP
  }),
  {
    changeUserXP,
    clearRewards,
    fetchNotifications,
    loadMoreNotifications,
    loadMoreRewards
  }
)(MainFeeds);

function renderNotificationMessage(notification, myId) {
  const {
    contentId,
    discussionId,
    type,
    reward = {},
    rootType,
    rootTitle,
    rootId,
    userId,
    username,
    commentContent,
    rootCommentUploader,
    discussionTitle,
    discussionUploader
  } = notification;
  let action = '';
  let isReplyNotification =
    commentContent && Number(rootCommentUploader) === myId;
  let isDiscussionAnswerNotification =
    discussionTitle && discussionUploader === myId;
  if (isReplyNotification) {
    action = returnCommentActionText('reply');
  } else if (isDiscussionAnswerNotification) {
    action = returnCommentActionText('comment');
  } else {
    switch (type) {
      case 'reward':
        action = (
          <>
             9
                    ? Color.gold()
                    : reward.rewardAmount > 4
                      ? Color.orange()
                      : Color.lightBlue()
              }}
            >
              rewarded you{' '}
              {reward.rewardAmount === 1 ? 'a' : reward.rewardAmount}{' '}
              {reward.rewardType}
              {reward.rewardAmount > 1 ? 's' : ''}
            {' '}
            for
          </>
        );
        break;
      case 'like':
        action = 'likes';
        break;
      case 'comment':
        action = returnCommentActionText(
          rootType === 'question' || rootType === 'user' ? rootType : 'comment'
        );
        break;
      case 'discussion':
        action = 'added a discussion to';
        break;
      default:
        break;
    }
  }
  const target = `your ${
    isReplyNotification
      ? 'comment'
      : isDiscussionAnswerNotification
        ? 'discussion topic'
        : rootType === 'user'
          ? 'profile'
          : rootType
  }${rootType === 'user' && !isReplyNotification ? '' : ': '}`;
  let contentTitle = isReplyNotification
    ? commentContent
    : (isDiscussionAnswerNotification ? discussionTitle : rootTitle) || '';
  let title =
    contentTitle.length > 50
      ? contentTitle.substr(0, 50) + '...'
      : contentTitle;
  if (isReplyNotification) title = `"${title}"`;
  const content = {
    title,
    id: isReplyNotification
      ? contentId
      : type === 'discussion'
        ? contentId
        : isDiscussionAnswerNotification
          ? discussionId
          : rootId
  };
  return (

      <UsernameText
        user={{ id: userId, username: username }}
        color={Color.blue()}
      />

      {action} {target}
      {!(rootType === 'user' && !isReplyNotification) && (
        <ContentLink
          content={content}
          type={
            isReplyNotification
              ? 'comment'
              : type === 'discussion' || isDiscussionAnswerNotification
                ? 'discussion'
                : rootType
          }
        />
      )}

  );

  function returnCommentActionText(type) {
    const title =
      type === 'comment'
        ? 'commented on'
        : type === 'reply'
          ? 'replied to'
          : type === 'question'
            ? 'answered'
            : 'left a message on';
    return (
      <ContentLink
        style={{ color: Color.green() }}
        content={{ id: contentId, title }}
        type="comment"
      />
    );
  }
}
<|endoftext|>


<commit_before>/**
 * @module loader_type_bundle
 * @module Load BEM bundle (JS+CSS) from external URL.
 */

modules.define('loader_type_bundle', function(provide) {

var LOADING_TIMEOUT = 20000,
    doc = document,
    head,
    bundles = {},

    handleError = function(bundleId) {
        var bundleDesc = bundles[bundleId],
            fns = bundleDesc.errorFns, fn;

        clearTimeout(bundleDesc.timer);

        while(fn = fns.shift()) fn();
        delete bundles[bundleId];
    },

    appendCss = function(css) {
        var style = doc.createElement('style');
        style.type = 'text/css';
        head.insertBefore(style, head.firstChild); // ie needs to insert style before setting content
        style.styleSheet?
            style.styleSheet.cssText = css :
            style.appendChild(doc.createTextNode(css));
    },

    /**
     * Loads bundle
     * @exports
     * @param {String} id
     * @param {String} url
     * @param {Function} onSuccess
     * @param {Function} [onError]
     */
    load = function(id, url, onSuccess, onError) {
        var bundle = bundles[id];
        if(bundle) {
            if(bundle.successFns) { // bundle is being loaded
                bundle.successFns.push(onSuccess);
                onError && bundle.errorFns.push(onError);
            } else { // bundle was loaded before
                setTimeout(onSuccess, 0);
            }
            return;
        }

        var script = doc.createElement('script'),
            errorFn = function() {
                handleError(id);
            };

        script.type = 'text/javascript';
        script.charset = 'utf-8';
        script.src = url;
        script.onerror = errorFn; // for browsers that support
        setTimeout(function() {
            (head || (head = doc.getElementsByTagName('head')[0])).insertBefore(script, head.firstChild);
        }, 0);

        bundles[id] = {
            successFns : [onSuccess],
            errorFns : onError? [onError] : [],
            timer : setTimeout(errorFn, LOADING_TIMEOUT)
        };
    };

load._loaded = function(bundle) {
    var bundleDesc = bundles[bundle.id];

    clearTimeout(bundleDesc.timer);

    bundle.js && bundle.js();

    bundle.css && appendCss(bundle.css);

    if(bundle.hcss) {
        var styles = [],
            _ycssjs = window._ycssjs;

        bundle.hcss.forEach(function(hsh) {
            if(_ycssjs) {
                if(hsh[0] in _ycssjs) return;
                _ycssjs(hsh[0]);
            }

            styles.push(hsh[1]);
        });

        styles.length && appendCss(styles.join(''));
    }

    function onSuccess() {
        var fns = bundleDesc.successFns, fn;
        while(fn = fns.shift()) fn();
        delete bundleDesc.successFns;
    }

    modules.isDefined('i-bem__dom_init')?
        modules.require(['i-bem__dom_init'], onSuccess) :
        onSuccess();
};

provide(load);

});
<commit_msg>loader_type_bundle: execute js in global context (close #423)<commit_after>/**
 * @module loader_type_bundle
 * @module Load BEM bundle (JS+CSS) from external URL.
 */

modules.define('loader_type_bundle', function(provide) {

var LOADING_TIMEOUT = 20000,
    global = this.global,
    doc = document,
    head,
    bundles = {},

    handleError = function(bundleId) {
        var bundleDesc = bundles[bundleId],
            fns = bundleDesc.errorFns, fn;

        clearTimeout(bundleDesc.timer);

        while(fn = fns.shift()) fn();
        delete bundles[bundleId];
    },

    appendCss = function(css) {
        var style = doc.createElement('style');
        style.type = 'text/css';
        head.insertBefore(style, head.firstChild); // ie needs to insert style before setting content
        style.styleSheet?
            style.styleSheet.cssText = css :
            style.appendChild(doc.createTextNode(css));
    },

    /**
     * Loads bundle
     * @exports
     * @param {String} id
     * @param {String} url
     * @param {Function} onSuccess
     * @param {Function} [onError]
     */
    load = function(id, url, onSuccess, onError) {
        var bundle = bundles[id];
        if(bundle) {
            if(bundle.successFns) { // bundle is being loaded
                bundle.successFns.push(onSuccess);
                onError && bundle.errorFns.push(onError);
            } else { // bundle was loaded before
                setTimeout(onSuccess, 0);
            }
            return;
        }

        var script = doc.createElement('script'),
            errorFn = function() {
                handleError(id);
            };

        script.type = 'text/javascript';
        script.charset = 'utf-8';
        script.src = url;
        script.onerror = errorFn; // for browsers that support
        setTimeout(function() {
            (head || (head = doc.getElementsByTagName('head')[0])).insertBefore(script, head.firstChild);
        }, 0);

        bundles[id] = {
            successFns : [onSuccess],
            errorFns : onError? [onError] : [],
            timer : setTimeout(errorFn, LOADING_TIMEOUT)
        };
    };

load._loaded = function(bundle) {
    var bundleDesc = bundles[bundle.id];

    clearTimeout(bundleDesc.timer);

    bundle.js && bundle.js.call(global);

    bundle.css && appendCss(bundle.css);

    if(bundle.hcss) {
        var styles = [],
            _ycssjs = window._ycssjs;

        bundle.hcss.forEach(function(hsh) {
            if(_ycssjs) {
                if(hsh[0] in _ycssjs) return;
                _ycssjs(hsh[0]);
            }

            styles.push(hsh[1]);
        });

        styles.length && appendCss(styles.join(''));
    }

    function onSuccess() {
        var fns = bundleDesc.successFns, fn;
        while(fn = fns.shift()) fn();
        delete bundleDesc.successFns;
    }

    modules.isDefined('i-bem__dom_init')?
        modules.require(['i-bem__dom_init'], onSuccess) :
        onSuccess();
};

provide(load);

});
<|endoftext|>


<commit_before>#!/usr/bin/env node

const hyperquest  = require('hyperzip')(require('hyperdirect'))
    , bl          = require('bl')
    , fs          = require('fs')
    , path        = require('path')
    , cheerio     = require('cheerio')
    , encoding    = 'utf8'
    , nodeVersion = process.argv[2]
    , nodeVersionRegexString = '\\d+\\.\\d+\\.\\d+'
    , files       = require('./files')
    , testReplace = require('./test-replacements')
    , docReplace  = require('./doc-replacements')

    , srcurlpfx   = `https://raw.githubusercontent.com/nodejs/node/v${nodeVersion}/`
    , libsrcurl   = srcurlpfx + 'lib/'
    , testsrcurl  = srcurlpfx + 'test/parallel/'
    , testlisturl = `https://github.com/nodejs/node/tree/v${nodeVersion}/test/parallel`
    , libourroot  = path.join(__dirname, '../lib/')
    , testourroot = path.join(__dirname, '../test/parallel/')
    , docurlpfx   = `https://raw.githubusercontent.com/nodejs/node/v${nodeVersion}/doc/api/`
    , docourroot  = path.join(__dirname, '../doc')

if (!RegExp('^' + nodeVersionRegexString + '$').test(nodeVersion)) {
  console.error('Usage: build.js xx.yy.zz')
  return process.exit(1);
}

function processFile (url, out, replacements) {
  hyperquest(url).pipe(bl(function (err, data) {
    if (err) throw err

    data = data.toString()
    replacements.forEach(function (replacement) {
      data = data.replace.apply(data, replacement)
    })

    fs.writeFile(out, data, encoding, function (err) {
      if (err) throw err

      console.log('Wrote', out)
    })
  }))
}

function processLibFile (file) {
  var replacements = files[file]
    , url          = libsrcurl + file
    , out          = path.join(libourroot, file)

  processFile(url, out, replacements)
}

function processTestFile (file) {
  var replacements = testReplace.all
    , url          = testsrcurl + file
    , out          = path.join(testourroot, file)

  if (testReplace[file])
    replacements = replacements.concat(testReplace[file])

  processFile(url, out, replacements)
}

//--------------------------------------------------------------------
// Grab & process files in ../lib/

Object.keys(files).forEach(processLibFile)

//--------------------------------------------------------------------
// Discover, grab and process all test-stream* files on nodejs/node

hyperquest(testlisturl).pipe(bl(function (err, data) {
  if (err)
    throw err

  var $ = cheerio.load(data.toString())

  $('table.files .js-directory-link').each(function () {
    var file = $(this).text()
    if (/^test-stream/.test(file) && !/-wrap\.js$/.test(file))
      processTestFile(file)
  })
}))

processFile(docurlpfx + 'stream.markdown', path.join(docourroot, 'stream.markdown'), docReplace)

//--------------------------------------------------------------------
// Grab the nodejs/node test/common.js

processFile(
    testsrcurl.replace(/parallel\/$/, 'common.js')
  , path.join(testourroot, '../common.js')
  , testReplace['common.js']
)
<commit_msg>Make processFile() accept URL or local path.<commit_after>#!/usr/bin/env node

const hyperquest  = require('hyperzip')(require('hyperdirect'))
    , bl          = require('bl')
    , fs          = require('fs')
    , path        = require('path')
    , cheerio     = require('cheerio')
    , encoding    = 'utf8'
    , nodeVersion = process.argv[2]
    , nodeVersionRegexString = '\\d+\\.\\d+\\.\\d+'
    , files       = require('./files')
    , testReplace = require('./test-replacements')
    , docReplace  = require('./doc-replacements')

    , srcurlpfx   = `https://raw.githubusercontent.com/nodejs/node/v${nodeVersion}/`
    , libsrcurl   = srcurlpfx + 'lib/'
    , testsrcurl  = srcurlpfx + 'test/parallel/'
    , testlisturl = `https://github.com/nodejs/node/tree/v${nodeVersion}/test/parallel`
    , libourroot  = path.join(__dirname, '../lib/')
    , testourroot = path.join(__dirname, '../test/parallel/')
    , docurlpfx   = `https://raw.githubusercontent.com/nodejs/node/v${nodeVersion}/doc/api/`
    , docourroot  = path.join(__dirname, '../doc')

if (!RegExp('^' + nodeVersionRegexString + '$').test(nodeVersion)) {
  console.error('Usage: build.js xx.yy.zz')
  return process.exit(1);
}

// `inputLoc`: URL or local path.
function processFile (inputLoc, out, replacements) {
  var file = /^https?:\/\//.test(inputLoc) ?
    hyperquest(inputLoc) :
    fs.createReadStream(inputLoc, encoding)

  file.pipe(bl(function (err, data) {
    if (err) throw err

    data = data.toString()
    replacements.forEach(function (replacement) {
      data = data.replace.apply(data, replacement)
    })

    fs.writeFile(out, data, encoding, function (err) {
      if (err) throw err

      console.log('Wrote', out)
    })
  }))
}

function processLibFile (file) {
  var replacements = files[file]
    , url          = libsrcurl + file
    , out          = path.join(libourroot, file)

  processFile(url, out, replacements)
}

function processTestFile (file) {
  var replacements = testReplace.all
    , url          = testsrcurl + file
    , out          = path.join(testourroot, file)

  if (testReplace[file])
    replacements = replacements.concat(testReplace[file])

  processFile(url, out, replacements)
}

//--------------------------------------------------------------------
// Grab & process files in ../lib/

Object.keys(files).forEach(processLibFile)

//--------------------------------------------------------------------
// Discover, grab and process all test-stream* files on nodejs/node

hyperquest(testlisturl).pipe(bl(function (err, data) {
  if (err)
    throw err

  var $ = cheerio.load(data.toString())

  $('table.files .js-directory-link').each(function () {
    var file = $(this).text()
    if (/^test-stream/.test(file) && !/-wrap\.js$/.test(file))
      processTestFile(file)
  })
}))

processFile(docurlpfx + 'stream.markdown', path.join(docourroot, 'stream.markdown'), docReplace)

//--------------------------------------------------------------------
// Grab the nodejs/node test/common.js

processFile(
    testsrcurl.replace(/parallel\/$/, 'common.js')
  , path.join(testourroot, '../common.js')
  , testReplace['common.js']
)
<|endoftext|>


<commit_before>/*!
 * Universidad Industrial de Santander
 * Grupo de Desarrollo de Software Calumet
 * Realtime | Sockets | Aula
 * Romel Pérez, prhone.blogspot.com
 * 2015
 **/

var _ = require('underscore');
var async = require('async');
var db = require('../databases/dbs.aula');
var config = require('../config');
var security = require('../security');
var debug = require('debug')('socket:aula');

// -------------------------------------------------------------------------- //
// AULA APPLICATION //

var aula = {

  io: null,
  express: null,
  roomsIntervalUpdate: null,

  /**
   * Proceso de validación de conexión del socket con el namespace aula.
   * Si abre más de una conexión con el aula, no se permitirá la segunda.
   * @param {Object} socket Es la conexión del socket estableciéndose
   * @param {Object} authorize Callback a ejecutar con la respuesta de conexión
   * Las posibles respuestas al usuario por la conexión son:
   * > ERROR       Error en el proceso de conexión
   * > ERROR_DATA  Datos erroneos
   * > DUPLICATE   Segunda conexión del aula en la misma computadora
   * > undefined   Usuario aceptado
   */
  handshake: function (socket, authorize) {

    var accept = function (res) {
      debug(socket.user.ip +' '+ socket.user.id +' '+ socket.id +' '
       + (res ? res : 'AUTHORIZED'));
      authorize(res ? {data: res} : res);
    };

    // Consiguiendo los datos de la clase.
    var userid = socket.user.id;
    var subject = socket.user.subject = socket.handshake.query.subject;
    var group = socket.user.group = socket.handshake.query.group;
    var subgroup = socket.user.subgroup = socket.handshake.query.subgroup;

    // Conseguir datos de sala de chat de la clase.
    db.rubi.ac_rooms.findById(subject +'_'+ group, function (err, classRoom) {
      if (err) {
        accept('ERROR');
        debug(err);
        return;
      }

      // No encontrado.
      if (!classRoom) {
        accept('ERROR_DATA');
        return;
      }

      // Saber si el usuario está conectado ya, verificando en la sala de clase:
      // rubi.ac_rooms.MATERIA_GRUPO.
      var st = _.findWhere(classRoom.students, {_id: userid});
      var f = (st && st.state === 'available') ||
      (userid === classRoom.teacher._id && classRoom.teacher.state === 'available');

      // Fue duplicado o está diśponible para conectarse.
      f ? accept('DUPLICATE') : accept();
    });
  },

  /**
   * Conexión por socket con el usuario ".on('connect', connect)".
   * @param {Object} socket El socket de conexión con el usuario
   */
  connect: function (socket) {

    // Usuario conectado en el aula.
    debug(socket.user.ip +' '+ socket.user.id +' '+ socket.id +' CONNECTED.');

    // Usuario conectado.
    aula.events.online.apply(socket);

    // Usuario desconectado (manualmente o automáticamente).
    socket.on('offline', function () {
      socket.disconnect(true);
    });
    socket.on('disconnect', function () {
      aula.events.offline.apply(socket, arguments);
      debug(socket.user.ip +' '+ socket.user.id +' '+ socket.id +' DISCONNECTED.');
    });

    // Mensaje de usuario.
    socket.on('msg', function () {
      aula.events.msg.apply(socket, arguments);
    });

  },

  /**
   * Eventos recibidos de un socket.
   * Emits posibles desde este evento de socket:
   * > serverError - El servidor tuvo un error procesando un evento de un usuario.
   */
  events: {

    /**
     * El usuario envió los datos para conectarse en el chat de clase, ya estándo
     * conectado con el socket.
     * Emits posibles desde este evento de socket:
     * > online - Se le envía al mismo usuario respondiendo que está online exitosamente.
     * > userOnline - Se le envía a todos los demás usuarios conectados en la sala.
     */
    online: function () {
      var socket = this;

      // Agregar a rubi como conectado.
      db.userConnected(socket, function (err, rooms, users) {
        if (err) {
          debug(err);
          return socket.emit('serverError', {ev: 'online'});
        }

        // Filtrar datos de salas de chat en las que se encuentra.
        var roomsFiltered = _.map(rooms, function (room) {
          return {
            id: room._id,
            available: room.available,
            teacher: {
              id: room.teacher._id,
              state: room.teacher.state
            },
            students: _.map(room.students, function (student) {
              return {
                id: student._id,
                state: student.state
              };
            }),
            messages: _.map(room.messages, function (msg) {
              return {
                id: msg._id,
                user: msg.user,
                content: msg.content
              };
            })
          };
        });

        // Filtrar datos de usuarios de cada sala de chat en la que se encuentra.
        var usersFiltered = _.map(users, function (user) {
          return {
            id: user._id,
            photo: user.photo,
            name: user.name
          };
        });

        // Enviarle la confirmación de que ya está conectado, con los datos de
        // las salas en las que se encuentra y los datos de todos sus usuarios.
        socket.emit('online', {
          rooms: roomsFiltered,
          users: usersFiltered
        });

        // Por cada sala de chat.
        _.each(rooms, function (room) {

          // Unirse a la sala.
          socket.join(room._id, function (err) {
            if (err) {
              debug(err);
              return socket.emit('serverError', {ev: 'online'});
            }

            // Cuando se una, informarle a los demás que está conectado.
            socket.to(room._id).emit('userOnline', {
              id: socket.user.id,
              room: room._id
            });
          });
        });
      });
    },

    /**
     * El usuario en cuestión se ha desconectado (manual/automáticamente).
     * Emits posibles desde este evento de socket:
     * > userOffline - Otro usuario se ha desconectado
     */
    offline: function () {
      var socket = this;

      // Colocar en rubi como desconectado.
      db.userDisconnected(socket, function (err, rooms) {
        if (err) {
          debug(err);
          return socket.emit('serverError', {ev: 'offline'});
        }

        // NOTE: No es necesario enviarle nada al usuario desconectado.

        // NOTE: El usuario deja la salas de chat cuando se desconecta
        // automáticamente.

        // Enviar a los de las salas, que se ha desconectado.
        _.each(rooms, function (room) {
          socket.to(room._id).emit('userOffline', {
            id: socket.user.id,
            room: room._id
          });
        });
      });

    },

    /**
     * Mensaje enviado por el usuario a una sala de chat.
     * @param {Object} msg {room:String, content:String}
     * Emits posibles desde este evento de socket:
     * > userMsg - Un usuario ha enviado un mensaje a una sala (clase/subgrupo),
     *   el cual puede ser el mismo usuario u otro en la sala.
     * > roomState - Una sala cambia de estado de actividad. Ex: Pasa de disponible
     *   a no disponible por un exámen.
     */
    msg: function (msg) {
      var socket = this;

      // El momento de ocurrido el mensaje se consigue en el servidor.
      var now = new Date().getTime();

      // Utilizar el contenido de llegada y enviarlo como llegó.
      // FIXIT: comprobar que funciona correctamente desde Western (ISO 8859-1)
      // al encoding del servidor que es UTF-8.
      var content = msg.content;

      // TODO: mantener los mensajes de la sala a menos de Number(X), eliminando
      // los mensajes más antiguos.

      // Buscar disponibilidad de sala.
      db.rubi.ac_rooms.findOne({_id: msg.room}, function (err, room) {
        if (err) {
          debug(err);
          return socket.emit('serverError', {ev: 'msg'});
        }

        // Sala no disponible. Enviar al usuario un mensaje indicandolo.
        if (!room.available) {
          return socket.emit('roomState', {
            room: msg.room,
            available: false
          });
        }

        // Guadar copia del mensaje en la sala de rubi.ac_rooms.
        db.addMessage(msg.room, {
          _id: now,
          user: socket.user.id,
          content: content
        }, function (err) {
          if (err) {
            debug(err);
            return socket.emit('serverError', {ev: 'msg'});
          }

          // Enviar a todos los usuarios del namespace/sala el mensaje.
          aula.io.of('/aula').to(msg.room).emit('userMsg', {
            id: now,
            user: socket.user.id,
            content: content
          });
        });
      });

    }

  },

  /**
   * Actualizar disponibilidad de las salas de clase y subgroupos de clase.
   */
  updateRoomsAvailability: function () {

    // Conseguir todas las salas tipo clase.
    db.rubi.ac_rooms.find({
      type: 'class'
    }, function (err, classes) {
      if (err) debug(err);

      // Recorrer cada clase (en serie para dividir trabajo).
      async.eachSeries(classes, function (clss, next) {

        // Materia y grupo.
        if (clss._id.indexOf('_') === -1) return next();
        var s = clss._id.substring(0, clss._id.indexOf('_'));
        var g = clss._id.substring(clss._id.indexOf('_') + 1);

        // Comprobar la disponibilidad de la clase.
        db.classInExam(s, g, function (err, inExam) {
          if (err) debug(err);

          // Actualizar de acuerdo al estado de la clase.
          db.rubi.ac_rooms.update({
            _id: new RegExp('^'+ s +'_'+ g)
          }, {
            available: !inExam
          }, next);
        });
      }, function (err) {
        if (err) debug(err);
      });
    });
  }

};

// -------------------------------------------------------------------------- //
// EVENTS //

module.exports = function (authorization) {

  aula.io = this.io;
  aula.express = this.express;

  // Implementar aplicación/namespace de sockets conectándose al aula.
  aula.io.of('/aula', aula.connect)
  .use(authorization.handshake)
  .use(aula.handshake);

  // Actualizar estado de las salas cada 1 minuto. Ex: Salas en exámen.
  aula.roomsIntervalUpdate = setInterval(aula.updateRoomsAvailability, 60000);
};
<commit_msg>fixed: enviando el dato de la sala de la cual se envía un mensaje<commit_after>/*!
 * Universidad Industrial de Santander
 * Grupo de Desarrollo de Software Calumet
 * Realtime | Sockets | Aula
 * Romel Pérez, prhone.blogspot.com
 * 2015
 **/

var _ = require('underscore');
var async = require('async');
var db = require('../databases/dbs.aula');
var config = require('../config');
var security = require('../security');
var debug = require('debug')('socket:aula');

// -------------------------------------------------------------------------- //
// AULA APPLICATION //

var aula = {

  io: null,
  express: null,
  roomsIntervalUpdate: null,

  /**
   * Proceso de validación de conexión del socket con el namespace aula.
   * Si abre más de una conexión con el aula, no se permitirá la segunda.
   * @param {Object} socket Es la conexión del socket estableciéndose
   * @param {Object} authorize Callback a ejecutar con la respuesta de conexión
   * Las posibles respuestas al usuario por la conexión son:
   * > ERROR       Error en el proceso de conexión
   * > ERROR_DATA  Datos erroneos
   * > DUPLICATE   Segunda conexión del aula en la misma computadora
   * > undefined   Usuario aceptado
   */
  handshake: function (socket, authorize) {

    var accept = function (res) {
      debug(socket.user.ip +' '+ socket.user.id +' '+ socket.id +' '
       + (res ? res : 'AUTHORIZED'));
      authorize(res ? {data: res} : res);
    };

    // Consiguiendo los datos de la clase.
    var userid = socket.user.id;
    var subject = socket.user.subject = socket.handshake.query.subject;
    var group = socket.user.group = socket.handshake.query.group;
    var subgroup = socket.user.subgroup = socket.handshake.query.subgroup;

    // Conseguir datos de sala de chat de la clase.
    db.rubi.ac_rooms.findById(subject +'_'+ group, function (err, classRoom) {
      if (err) {
        accept('ERROR');
        debug(err);
        return;
      }

      // No encontrado.
      if (!classRoom) {
        accept('ERROR_DATA');
        return;
      }

      // Saber si el usuario está conectado ya, verificando en la sala de clase:
      // rubi.ac_rooms.MATERIA_GRUPO.
      var st = _.findWhere(classRoom.students, {_id: userid});
      var f = (st && st.state === 'available') ||
      (userid === classRoom.teacher._id && classRoom.teacher.state === 'available');

      // Fue duplicado o está diśponible para conectarse.
      f ? accept('DUPLICATE') : accept();
    });
  },

  /**
   * Conexión por socket con el usuario ".on('connect', connect)".
   * @param {Object} socket El socket de conexión con el usuario
   */
  connect: function (socket) {

    // Usuario conectado en el aula.
    debug(socket.user.ip +' '+ socket.user.id +' '+ socket.id +' CONNECTED.');

    // Usuario conectado.
    aula.events.online.apply(socket);

    // Usuario desconectado (manualmente o automáticamente).
    socket.on('offline', function () {
      socket.disconnect(true);
    });
    socket.on('disconnect', function () {
      aula.events.offline.apply(socket, arguments);
      debug(socket.user.ip +' '+ socket.user.id +' '+ socket.id +' DISCONNECTED.');
    });

    // Mensaje de usuario.
    socket.on('msg', function () {
      aula.events.msg.apply(socket, arguments);
    });

  },

  /**
   * Eventos recibidos de un socket.
   * Emits posibles desde este evento de socket:
   * > serverError - El servidor tuvo un error procesando un evento de un usuario.
   */
  events: {

    /**
     * El usuario envió los datos para conectarse en el chat de clase, ya estándo
     * conectado con el socket.
     * Emits posibles desde este evento de socket:
     * > online - Se le envía al mismo usuario respondiendo que está online exitosamente.
     * > userOnline - Se le envía a todos los demás usuarios conectados en la sala.
     */
    online: function () {
      var socket = this;

      // Agregar a rubi como conectado.
      db.userConnected(socket, function (err, rooms, users) {
        if (err) {
          debug(err);
          return socket.emit('serverError', {ev: 'online'});
        }

        // Filtrar datos de salas de chat en las que se encuentra.
        var roomsFiltered = _.map(rooms, function (room) {
          return {
            id: room._id,
            available: room.available,
            teacher: {
              id: room.teacher._id,
              state: room.teacher.state
            },
            students: _.map(room.students, function (student) {
              return {
                id: student._id,
                state: student.state
              };
            }),
            messages: _.map(room.messages, function (msg) {
              return {
                id: msg._id,
                user: msg.user,
                content: msg.content
              };
            })
          };
        });

        // Filtrar datos de usuarios de cada sala de chat en la que se encuentra.
        var usersFiltered = _.map(users, function (user) {
          return {
            id: user._id,
            photo: user.photo,
            name: user.name
          };
        });

        // Enviarle la confirmación de que ya está conectado, con los datos de
        // las salas en las que se encuentra y los datos de todos sus usuarios.
        socket.emit('online', {
          rooms: roomsFiltered,
          users: usersFiltered
        });

        // Por cada sala de chat.
        _.each(rooms, function (room) {

          // Unirse a la sala.
          socket.join(room._id, function (err) {
            if (err) {
              debug(err);
              return socket.emit('serverError', {ev: 'online'});
            }

            // Cuando se una, informarle a los demás que está conectado.
            socket.to(room._id).emit('userOnline', {
              id: socket.user.id,
              room: room._id
            });
          });
        });
      });
    },

    /**
     * El usuario en cuestión se ha desconectado (manual/automáticamente).
     * Emits posibles desde este evento de socket:
     * > userOffline - Otro usuario se ha desconectado
     */
    offline: function () {
      var socket = this;

      // Colocar en rubi como desconectado.
      db.userDisconnected(socket, function (err, rooms) {
        if (err) {
          debug(err);
          return socket.emit('serverError', {ev: 'offline'});
        }

        // NOTE: No es necesario enviarle nada al usuario desconectado.

        // NOTE: El usuario deja la salas de chat cuando se desconecta
        // automáticamente.

        // Enviar a los de las salas, que se ha desconectado.
        _.each(rooms, function (room) {
          socket.to(room._id).emit('userOffline', {
            id: socket.user.id,
            room: room._id
          });
        });
      });

    },

    /**
     * Mensaje enviado por el usuario a una sala de chat.
     * @param {Object} msg {room:String, content:String}
     * Emits posibles desde este evento de socket:
     * > userMsg - Un usuario ha enviado un mensaje a una sala (clase/subgrupo),
     *   el cual puede ser el mismo usuario u otro en la sala.
     * > roomState - Una sala cambia de estado de actividad. Ex: Pasa de disponible
     *   a no disponible por un exámen.
     */
    msg: function (msg) {
      var socket = this;

      // El momento de ocurrido el mensaje se consigue en el servidor.
      var now = new Date().getTime();

      // Utilizar el contenido de llegada y enviarlo como llegó.
      // FIXIT: comprobar que funciona correctamente desde Western (ISO 8859-1)
      // al encoding del servidor que es UTF-8.
      var content = msg.content;

      // TODO: mantener los mensajes de la sala a menos de Number(X), eliminando
      // los mensajes más antiguos.

      // Buscar disponibilidad de sala.
      db.rubi.ac_rooms.findOne({_id: msg.room}, function (err, room) {
        if (err) {
          debug(err);
          return socket.emit('serverError', {ev: 'msg'});
        }

        // Sala no disponible. Enviar al usuario un mensaje indicandolo.
        if (!room.available) {
          return socket.emit('roomState', {
            room: msg.room,
            available: false
          });
        }

        // Guadar copia del mensaje en la sala de rubi.ac_rooms.
        db.addMessage(msg.room, {
          _id: now,
          user: socket.user.id,
          content: content
        }, function (err) {
          if (err) {
            debug(err);
            return socket.emit('serverError', {ev: 'msg'});
          }

          // Enviar a todos los usuarios del namespace/sala el mensaje.
          aula.io.of('/aula').to(msg.room).emit('userMsg', {
            id: now,
            room: msg.room,
            user: socket.user.id,
            content: content
          });
        });
      });

    }

  },

  /**
   * Actualizar disponibilidad de las salas de clase y subgroupos de clase.
   */
  updateRoomsAvailability: function () {

    // Conseguir todas las salas tipo clase.
    db.rubi.ac_rooms.find({
      type: 'class'
    }, function (err, classes) {
      if (err) debug(err);

      // Recorrer cada clase (en serie para dividir trabajo).
      async.eachSeries(classes, function (clss, next) {

        // Materia y grupo.
        if (clss._id.indexOf('_') === -1) return next();
        var s = clss._id.substring(0, clss._id.indexOf('_'));
        var g = clss._id.substring(clss._id.indexOf('_') + 1);

        // Comprobar la disponibilidad de la clase.
        db.classInExam(s, g, function (err, inExam) {
          if (err) debug(err);

          // Actualizar de acuerdo al estado de la clase.
          db.rubi.ac_rooms.update({
            _id: new RegExp('^'+ s +'_'+ g)
          }, {
            available: !inExam
          }, next);
        });
      }, function (err) {
        if (err) debug(err);
      });
    });
  }

};

// -------------------------------------------------------------------------- //
// EVENTS //

module.exports = function (authorization) {

  aula.io = this.io;
  aula.express = this.express;

  // Implementar aplicación/namespace de sockets conectándose al aula.
  aula.io.of('/aula', aula.connect)
  .use(authorization.handshake)
  .use(aula.handshake);

  // Actualizar estado de las salas cada 1 minuto. Ex: Salas en exámen.
  aula.roomsIntervalUpdate = setInterval(aula.updateRoomsAvailability, 60000);
};
<|endoftext|>