<commit_before>/*    Copyright 2016 Firewalla LLC
 *
 *    This program is free software: you can redistribute it and/or  modify
 *    it under the terms of the GNU Affero General Public License, version 3,
 *    as published by the Free Software Foundation.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU Affero General Public License for more details.
 *
 *    You should have received a copy of the GNU Affero General Public License
 *    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
'use strict';

const log = require('../net2/logger.js')(__filename);

let util = require('util');

let Sensor = require('./Sensor.js').Sensor;

const rclient = require('../util/redis_manager.js').getRedisClient()
const sclient = require('../util/redis_manager.js').getSubscriptionClient()

const PolicyManager2 = require('../alarm/PolicyManager2.js')
const pm2 = new PolicyManager2()

const ExceptionManager = require('../alarm/ExceptionManager.js')
const em = new ExceptionManager()

const HostTool = require('../net2/HostTool.js')
const hostTool = new HostTool();

const AlarmManager2 = require('../alarm/AlarmManager2.js');
const am2 = new AlarmManager2();

let Promise = require('bluebird');

let async = require('asyncawait/async');
let await = require('asyncawait/await');

const migrationPrefix = "oldDataMigration";

let fConfig = require('../net2/config.js').getConfig();

function arrayDiff(a, b) {
  return a.filter(function(i) {return b.indexOf(i) < 0;});
};

class OldDataCleanSensor extends Sensor {
  constructor() {
    super();
  }

  getExpiredDate(type) {
    let expireInterval = (this.config[type] && this.config[type].expires) || 0;
    if(expireInterval < 0) {
      return null;
    }

    let minInterval = 30 * 60;
    expireInterval = Math.max(expireInterval, minInterval);

    return Date.now() / 1000 - expireInterval;
  }

  getCount(type) {
    let count = (this.config[type] && this.config[type].count) || 10000;
    if(count < 0) {
      return null;
    }
    return count;
  }

  async cleanByExpireDate(key, expireDate) {
    const count = await rclient.zremrangebyscoreAsync(key, "-inf", expireDate);
    if(count > 10) {
      log.info(util.format("%d entries in %s are cleaned by expired date", count, key));
    }
  }

  async cleanToCount(key, leftOverCount) {
    const count = await rclient.zremrangebyrankAsync(key, 0, -1 * leftOverCount)
    if(count > 10) {
      log.info(util.format("%d entries in %s are cleaned by count", count, key));
    }
  }

  getKeys(keyPattern) {
    return rclient.keysAsync(keyPattern);
  }

  // clean by expired time and count
  async regularClean(type, keyPattern, ignorePatterns) {
      let keys = await (this.getKeys(keyPattern));

      if(ignorePatterns) {
        keys = keys.filter((x) => {
          return ignorePatterns.filter((p) => x.match(p)).length === 0
        });
      }

      for (let index = 0; index < keys.length; index++) {
        const key = keys[index];
        const expireDate = this.getExpiredDate(type);
        if(expireDate !== null) {
          await this.cleanByExpireDate(key, expireDate);
        }
        const count = this.getCount(type);
        await this.cleanToCount(key, count);
      }
  }

  cleanAlarm() {
    // TODO
  }

  cleanPolicy() {
    // TODO
  }

  cleanException() {
    // TODO
  }

  cleanSumFlow() {

  }

  cleanHourlyFlow() {

  }

  cleanAggrFlow() {

  }

  async cleanHourlyStats() {
    // FIXME: not well coded here, deprecated code
    let keys = await rclient.keysAsync("stats:hour*");
    let expireDate = Date.now() / 1000 - 60 * 60 * 24 * 30 * 6;
    for (let j in keys) {
      rclient.zscan(keys[j],0,(err,data)=>{
        if (data && data.length==2) {
          let array = data[1];
          for (let i=0;i<array.length;i++) {
            if (array[i]<expireDate) {
              rclient.zrem(keys[j],array[i]);
            }
            i += Number(1);
          }
        }
      });
    }
    // expire legacy stats:last24 keys if its expiration is not set
    keys = await rclient.keysAsync("stats:last24:*");
    for (let j in keys) {
      const key = keys[j];
      const ttl = await rclient.ttlAsync(key);
      if (ttl === -1) {
        // legacy last 24 hour stats record, need to expire it.
        await rclient.expireAsync(key, 3600 * 24);
      }
    }
  }

  cleanUserAgents() {
    // FIXME: not well coded here, deprecated code
      let MAX_AGENT_STORED = 150;
      rclient.keys("host:user_agent:*", (err, keys) => {
        for (let j in keys) {
          rclient.scard(keys[j], (err, count) => {
//                    log.info(keys[j]," count ", count);
            if (count > MAX_AGENT_STORED) {
              log.info(keys[j], " pop count ", count - MAX_AGENT_STORED);
              for (let i = 0; i < count - MAX_AGENT_STORED; i++) {
                rclient.spop(keys[j], (err) => {
                  if (err) {
                    log.info(keys[j], " count ", count - MAX_AGENT_STORED, err);
                  }
                });
              }
            }
          });
        }
      });

      return Promise.resolve();
  }

  cleanHostData(type, keyPattern, defaultExpireInterval) {
    let expireInterval = (this.config[type] && this.config[type].expires) ||
      defaultExpireInterval;

    let expireDate = Date.now() / 1000 - expireInterval;

    return this.getKeys(keyPattern)
      .then((keys) => {
        return Promise.all(
          keys.map((key) => {
            return rclient.hgetallAsync(key)
              .then((data) => {
                if (data &&  data.lastActiveTimestamp) {
                  if (data.lastActiveTimestamp < expireDate) {
                    log.info(key,"Deleting due to timeout ", expireDate, data);
                    return rclient.delAsync(key);
                  } else {
                    return Promise.resolve();
                  }
                } else {
                  return Promise.resolve();
                }
              })
          })
        ).then(() => {
          // log.info("CleanHostData on", keys, "is completed", {});
        })
      });
  }

  cleanDuplicatedPolicy() {
    return async(() => {

      const policies = await (pm2.loadActivePolicysAsync(1000))

      let toBeDeleted = []

      for(let i = 0; i < policies.length; i++) {
        let p = policies[i]
        for(let j = i+1; j< policies.length; j++) {
          let p2 = policies[j]
          if(p && p2 && p.isEqualToPolicy(p2)) {
            toBeDeleted.push(p)
            break
          }
        }
      }

      for(let k in toBeDeleted) {
        let p = toBeDeleted[k]
        await (pm2.deletePolicy(p.pid))
      }
    })()
  }

  cleanDuplicatedException() {
    return async(() => {
      let exceptions = [];
      try {
        exceptions = await(em.loadExceptionsAsync());
      } catch (err) {
        log.error("Error when loadExceptions", err);
      }

      let toBeDeleted = []

      for(let i = 0; i < exceptions.length; i++) {
        let e = exceptions[i]
        for(let j = i+1; j< exceptions.length; j++) {
          let e2 = exceptions[j]
          if(e && e2 && e.isEqualToException(e2)) {
            toBeDeleted.push(e)
            break
          }
        }
      }

      for(let k in toBeDeleted) {
        let e = toBeDeleted[k]
        try {
          await(em.deleteException(e.eid))
        } catch (err) {
          log.error("Error when delete exception", err);
        }
      }
    })()
  }

  cleanInvalidMACAddress() {
    return async(() => {
      const macs = await (hostTool.getAllMACs())
      const invalidMACs = macs.filter((m) => {
        return m.match(/[a-f]+/) != null
      })
      invalidMACs.forEach((m) => {
        await (hostTool.deleteMac(m))
      })
    })()
  }

  async cleanupAlarmExtendedKeys() {
    log.info("Cleaning up alarm extended keys");

    const basicAlarms = await am2.listBasicAlarms();
    const extendedAlarms = await am2.listExtendedAlarms();

    const diff = arrayDiff(extendedAlarms, basicAlarms);

    for (let index = 0; index < diff.length; index++) {
      const alarmID = diff[index];
      await (am2.deleteExtendedAlarm(alarmID))
    }
  }

  // async cleanBlueRecords() {
  //   const keyPattern = "blue:history:domain:*"
  //   const keys = await rclient.keysAsync(keyPattern);
  //   for (let i = 0; i < keys.length; i++) {
  //     const key = keys[i];
  //     await rclient.zremrangebyscoreAsync(key, '-inf', Math.floor(new Date() / 1000 - 3600 * 48)) // keep two days
  //   }
  // }

  oneTimeJob() {
    return async(() => {
      await (this.cleanDuplicatedPolicy())
      await (this.cleanDuplicatedException())
      await (this.cleanInvalidMACAddress())
    })()
  }

  scheduledJob() {
    return async(() => {
      log.info("Start cleaning old data in redis")

      await (this.regularClean("conn", "flow:conn:*"));
      await (this.regularClean("ssl", "flow:ssl:*"));
      await (this.regularClean("http", "flow:http:*"));
      await (this.regularClean("notice", "notice:*"));
      await (this.regularClean("intel", "intel:*", [/^intel:ip/]));
      await (this.regularClean("software", "software:*"));
      await (this.regularClean("monitor", "monitor:flow:*"));
      await (this.regularClean("alarm", "alarm:ip4:*"));
      await (this.regularClean("sumflow", "sumflow:*"));
      await (this.regularClean("aggrflow", "aggrflow:*"));
      await (this.regularClean("syssumflow", "syssumflow:*"));
      await (this.cleanHourlyStats());
      await (this.cleanUserAgents());
      await (this.cleanHostData("host:ip4", "host:ip4:*", 60*60*24*30));
      await (this.cleanHostData("host:ip6", "host:ip6:*", 60*60*24*30));
      await (this.cleanHostData("host:mac", "host:mac:*", 60*60*24*365));

      await (this.cleanupAlarmExtendedKeys());

      // await (this.cleanBlueRecords())
      log.info("scheduledJob is executed successfully");
    })().catch((err) => {
      log.error("Failed to run scheduled job, err:", err);
    });
  }

  listen() {
    sclient.on("message", (channel, message) => {
      if(channel === "OldDataCleanSensor" && message === "Start") {
        this.scheduledJob();
      }
    });
    sclient.subscribe("OldDataCleanSensor");
    log.info("Listen on channel FlowDataCleanSensor");
  }

  // could be disabled in the future when all policy blockin rule is migrated to general policy rules
  hostPolicyMigration() {
    return async(() => {
      const keys = await (rclient.keysAsync("policy:mac:*"))
      if(keys) {
        keys.forEach((key) => {
          const blockin = await (rclient.hgetAsync(key, "blockin"))
          if(blockin && blockin == "true") {
            const mac = key.replace("policy:mac:", "")
            const rule = await (pm2.findPolicy(mac, "mac"))
            if(!rule) {
              log.info(`Migrating blockin policy for host ${mac} to policyRule`)
              const hostInfo = await (hostTool.getMACEntry(mac))
              const newRule = pm2.createPolicy({
                target: mac,
                type: "mac",
                target_name: hostInfo.name || hostInfo.bname || hostInfo.ipv4Addr,
                target_ip: hostInfo.ipv4Addr // target_name and target ip are necessary for old app display
              })
              const result = await (pm2.checkAndSaveAsync(newRule))
              if(result) {
                await (rclient.hsetAsync(key, "blockin", false))
                log.info("Migrated successfully")
              } else {
                log.error("Failed to migrate")
              }
            }
          }
        })
      }
    })().catch((err) => {
      log.error("Failed to migrate host policy rules:", err, {})
    })
  }

  async legacySchedulerMigration() {
    const key = `${migrationPrefix}:legacySchedulerMigration`;
    const result = await rclient.typeAsync(key);
    if(result !== "none") {
      return;
    }

    const policyRules = await pm2.loadActivePolicysAsync();
    for(const rule of policyRules) {
      if(rule.cronTime === "* * * * 1" && rule.duration === "432000") {
        rule.cronTime = "0 0 * * 1,2,3,4,5";
        rule.duration = "86390";
        await pm2.updatePolicyAsync(rule);
      } else if(rule.cronTime === "* * * * 6" && rule.duration === "172800") {
        rule.cronTime = "0 0 * * 0,6";
        rule.duration = "86390";
        await pm2.updatePolicyAsync(rule);
      }
    }

    await rclient.setAsync(key, "1");
    return;
  }

  run() {
    super.run();

    this.listen();

    this.hostPolicyMigration()

    this.legacySchedulerMigration();

    setTimeout(() => {
      this.scheduledJob();
      this.oneTimeJob()
      setInterval(() => {
        this.scheduledJob();
      }, 1000 * 60 * 60); // cleanup every hour
    }, 1000 * 60 * 5); // first time in 5 mins
  }
}

module.exports = OldDataCleanSensor;
<commit_msg>Cleanup categoryflow:* in OldDataCleanSensor<commit_after>/*    Copyright 2016 Firewalla LLC
 *
 *    This program is free software: you can redistribute it and/or  modify
 *    it under the terms of the GNU Affero General Public License, version 3,
 *    as published by the Free Software Foundation.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU Affero General Public License for more details.
 *
 *    You should have received a copy of the GNU Affero General Public License
 *    along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
'use strict';

const log = require('../net2/logger.js')(__filename);

let util = require('util');

let Sensor = require('./Sensor.js').Sensor;

const rclient = require('../util/redis_manager.js').getRedisClient()
const sclient = require('../util/redis_manager.js').getSubscriptionClient()

const PolicyManager2 = require('../alarm/PolicyManager2.js')
const pm2 = new PolicyManager2()

const ExceptionManager = require('../alarm/ExceptionManager.js')
const em = new ExceptionManager()

const HostTool = require('../net2/HostTool.js')
const hostTool = new HostTool();

const AlarmManager2 = require('../alarm/AlarmManager2.js');
const am2 = new AlarmManager2();

let Promise = require('bluebird');

let async = require('asyncawait/async');
let await = require('asyncawait/await');

const migrationPrefix = "oldDataMigration";

let fConfig = require('../net2/config.js').getConfig();

function arrayDiff(a, b) {
  return a.filter(function(i) {return b.indexOf(i) < 0;});
};

class OldDataCleanSensor extends Sensor {
  constructor() {
    super();
  }

  getExpiredDate(type) {
    let expireInterval = (this.config[type] && this.config[type].expires) || 0;
    if(expireInterval < 0) {
      return null;
    }

    let minInterval = 30 * 60;
    expireInterval = Math.max(expireInterval, minInterval);

    return Date.now() / 1000 - expireInterval;
  }

  getCount(type) {
    let count = (this.config[type] && this.config[type].count) || 10000;
    if(count < 0) {
      return null;
    }
    return count;
  }

  async cleanByExpireDate(key, expireDate) {
    const count = await rclient.zremrangebyscoreAsync(key, "-inf", expireDate);
    if(count > 10) {
      log.info(util.format("%d entries in %s are cleaned by expired date", count, key));
    }
  }

  async cleanToCount(key, leftOverCount) {
    const count = await rclient.zremrangebyrankAsync(key, 0, -1 * leftOverCount)
    if(count > 10) {
      log.info(util.format("%d entries in %s are cleaned by count", count, key));
    }
  }

  getKeys(keyPattern) {
    return rclient.keysAsync(keyPattern);
  }

  // clean by expired time and count
  async regularClean(type, keyPattern, ignorePatterns) {
      let keys = await (this.getKeys(keyPattern));

      if(ignorePatterns) {
        keys = keys.filter((x) => {
          return ignorePatterns.filter((p) => x.match(p)).length === 0
        });
      }

      for (let index = 0; index < keys.length; index++) {
        const key = keys[index];
        const expireDate = this.getExpiredDate(type);
        if(expireDate !== null) {
          await this.cleanByExpireDate(key, expireDate);
        }
        const count = this.getCount(type);
        await this.cleanToCount(key, count);
      }
  }

  cleanAlarm() {
    // TODO
  }

  cleanPolicy() {
    // TODO
  }

  cleanException() {
    // TODO
  }

  cleanSumFlow() {

  }

  cleanHourlyFlow() {

  }

  cleanAggrFlow() {

  }

  async cleanHourlyStats() {
    // FIXME: not well coded here, deprecated code
    let keys = await rclient.keysAsync("stats:hour*");
    let expireDate = Date.now() / 1000 - 60 * 60 * 24 * 30 * 6;
    for (let j in keys) {
      rclient.zscan(keys[j],0,(err,data)=>{
        if (data && data.length==2) {
          let array = data[1];
          for (let i=0;i<array.length;i++) {
            if (array[i]<expireDate) {
              rclient.zrem(keys[j],array[i]);
            }
            i += Number(1);
          }
        }
      });
    }
    // expire legacy stats:last24 keys if its expiration is not set
    keys = await rclient.keysAsync("stats:last24:*");
    for (let j in keys) {
      const key = keys[j];
      const ttl = await rclient.ttlAsync(key);
      if (ttl === -1) {
        // legacy last 24 hour stats record, need to expire it.
        await rclient.expireAsync(key, 3600 * 24);
      }
    }
  }

  cleanUserAgents() {
    // FIXME: not well coded here, deprecated code
      let MAX_AGENT_STORED = 150;
      rclient.keys("host:user_agent:*", (err, keys) => {
        for (let j in keys) {
          rclient.scard(keys[j], (err, count) => {
//                    log.info(keys[j]," count ", count);
            if (count > MAX_AGENT_STORED) {
              log.info(keys[j], " pop count ", count - MAX_AGENT_STORED);
              for (let i = 0; i < count - MAX_AGENT_STORED; i++) {
                rclient.spop(keys[j], (err) => {
                  if (err) {
                    log.info(keys[j], " count ", count - MAX_AGENT_STORED, err);
                  }
                });
              }
            }
          });
        }
      });

      return Promise.resolve();
  }

  cleanHostData(type, keyPattern, defaultExpireInterval) {
    let expireInterval = (this.config[type] && this.config[type].expires) ||
      defaultExpireInterval;

    let expireDate = Date.now() / 1000 - expireInterval;

    return this.getKeys(keyPattern)
      .then((keys) => {
        return Promise.all(
          keys.map((key) => {
            return rclient.hgetallAsync(key)
              .then((data) => {
                if (data &&  data.lastActiveTimestamp) {
                  if (data.lastActiveTimestamp < expireDate) {
                    log.info(key,"Deleting due to timeout ", expireDate, data);
                    return rclient.delAsync(key);
                  } else {
                    return Promise.resolve();
                  }
                } else {
                  return Promise.resolve();
                }
              })
          })
        ).then(() => {
          // log.info("CleanHostData on", keys, "is completed", {});
        })
      });
  }

  cleanDuplicatedPolicy() {
    return async(() => {

      const policies = await (pm2.loadActivePolicysAsync(1000))

      let toBeDeleted = []

      for(let i = 0; i < policies.length; i++) {
        let p = policies[i]
        for(let j = i+1; j< policies.length; j++) {
          let p2 = policies[j]
          if(p && p2 && p.isEqualToPolicy(p2)) {
            toBeDeleted.push(p)
            break
          }
        }
      }

      for(let k in toBeDeleted) {
        let p = toBeDeleted[k]
        await (pm2.deletePolicy(p.pid))
      }
    })()
  }

  cleanDuplicatedException() {
    return async(() => {
      let exceptions = [];
      try {
        exceptions = await(em.loadExceptionsAsync());
      } catch (err) {
        log.error("Error when loadExceptions", err);
      }

      let toBeDeleted = []

      for(let i = 0; i < exceptions.length; i++) {
        let e = exceptions[i]
        for(let j = i+1; j< exceptions.length; j++) {
          let e2 = exceptions[j]
          if(e && e2 && e.isEqualToException(e2)) {
            toBeDeleted.push(e)
            break
          }
        }
      }

      for(let k in toBeDeleted) {
        let e = toBeDeleted[k]
        try {
          await(em.deleteException(e.eid))
        } catch (err) {
          log.error("Error when delete exception", err);
        }
      }
    })()
  }

  cleanInvalidMACAddress() {
    return async(() => {
      const macs = await (hostTool.getAllMACs())
      const invalidMACs = macs.filter((m) => {
        return m.match(/[a-f]+/) != null
      })
      invalidMACs.forEach((m) => {
        await (hostTool.deleteMac(m))
      })
    })()
  }

  async cleanupAlarmExtendedKeys() {
    log.info("Cleaning up alarm extended keys");

    const basicAlarms = await am2.listBasicAlarms();
    const extendedAlarms = await am2.listExtendedAlarms();

    const diff = arrayDiff(extendedAlarms, basicAlarms);

    for (let index = 0; index < diff.length; index++) {
      const alarmID = diff[index];
      await (am2.deleteExtendedAlarm(alarmID))
    }
  }

  // async cleanBlueRecords() {
  //   const keyPattern = "blue:history:domain:*"
  //   const keys = await rclient.keysAsync(keyPattern);
  //   for (let i = 0; i < keys.length; i++) {
  //     const key = keys[i];
  //     await rclient.zremrangebyscoreAsync(key, '-inf', Math.floor(new Date() / 1000 - 3600 * 48)) // keep two days
  //   }
  // }

  oneTimeJob() {
    return async(() => {
      await (this.cleanDuplicatedPolicy())
      await (this.cleanDuplicatedException())
      await (this.cleanInvalidMACAddress())
    })()
  }

  scheduledJob() {
    return async(() => {
      log.info("Start cleaning old data in redis")

      await (this.regularClean("conn", "flow:conn:*"));
      await (this.regularClean("ssl", "flow:ssl:*"));
      await (this.regularClean("http", "flow:http:*"));
      await (this.regularClean("notice", "notice:*"));
      await (this.regularClean("intel", "intel:*", [/^intel:ip/]));
      await (this.regularClean("software", "software:*"));
      await (this.regularClean("monitor", "monitor:flow:*"));
      await (this.regularClean("alarm", "alarm:ip4:*"));
      await (this.regularClean("sumflow", "sumflow:*"));
      await (this.regularClean("aggrflow", "aggrflow:*"));
      await (this.regularClean("syssumflow", "syssumflow:*"));
      await (this.regularClean("categoryflow", "categoryflow:*"));
      await (this.cleanHourlyStats());
      await (this.cleanUserAgents());
      await (this.cleanHostData("host:ip4", "host:ip4:*", 60*60*24*30));
      await (this.cleanHostData("host:ip6", "host:ip6:*", 60*60*24*30));
      await (this.cleanHostData("host:mac", "host:mac:*", 60*60*24*365));

      await (this.cleanupAlarmExtendedKeys());

      // await (this.cleanBlueRecords())
      log.info("scheduledJob is executed successfully");
    })().catch((err) => {
      log.error("Failed to run scheduled job, err:", err);
    });
  }

  listen() {
    sclient.on("message", (channel, message) => {
      if(channel === "OldDataCleanSensor" && message === "Start") {
        this.scheduledJob();
      }
    });
    sclient.subscribe("OldDataCleanSensor");
    log.info("Listen on channel FlowDataCleanSensor");
  }

  // could be disabled in the future when all policy blockin rule is migrated to general policy rules
  hostPolicyMigration() {
    return async(() => {
      const keys = await (rclient.keysAsync("policy:mac:*"))
      if(keys) {
        keys.forEach((key) => {
          const blockin = await (rclient.hgetAsync(key, "blockin"))
          if(blockin && blockin == "true") {
            const mac = key.replace("policy:mac:", "")
            const rule = await (pm2.findPolicy(mac, "mac"))
            if(!rule) {
              log.info(`Migrating blockin policy for host ${mac} to policyRule`)
              const hostInfo = await (hostTool.getMACEntry(mac))
              const newRule = pm2.createPolicy({
                target: mac,
                type: "mac",
                target_name: hostInfo.name || hostInfo.bname || hostInfo.ipv4Addr,
                target_ip: hostInfo.ipv4Addr // target_name and target ip are necessary for old app display
              })
              const result = await (pm2.checkAndSaveAsync(newRule))
              if(result) {
                await (rclient.hsetAsync(key, "blockin", false))
                log.info("Migrated successfully")
              } else {
                log.error("Failed to migrate")
              }
            }
          }
        })
      }
    })().catch((err) => {
      log.error("Failed to migrate host policy rules:", err, {})
    })
  }

  async legacySchedulerMigration() {
    const key = `${migrationPrefix}:legacySchedulerMigration`;
    const result = await rclient.typeAsync(key);
    if(result !== "none") {
      return;
    }

    const policyRules = await pm2.loadActivePolicysAsync();
    for(const rule of policyRules) {
      if(rule.cronTime === "* * * * 1" && rule.duration === "432000") {
        rule.cronTime = "0 0 * * 1,2,3,4,5";
        rule.duration = "86390";
        await pm2.updatePolicyAsync(rule);
      } else if(rule.cronTime === "* * * * 6" && rule.duration === "172800") {
        rule.cronTime = "0 0 * * 0,6";
        rule.duration = "86390";
        await pm2.updatePolicyAsync(rule);
      }
    }

    await rclient.setAsync(key, "1");
    return;
  }

  run() {
    super.run();

    this.listen();

    this.hostPolicyMigration()

    this.legacySchedulerMigration();

    setTimeout(() => {
      this.scheduledJob();
      this.oneTimeJob()
      setInterval(() => {
        this.scheduledJob();
      }, 1000 * 60 * 60); // cleanup every hour
    }, 1000 * 60 * 5); // first time in 5 mins
  }
}

module.exports = OldDataCleanSensor;
<|endoftext|>


<commit_before>const webpack = require("webpack");
const CleanWebpackPlugin = require("clean-webpack-plugin");
const ProgressBarPlugin = require("progress-bar-webpack-plugin");
const WriteFilePlugin = require("write-file-webpack-plugin");
const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
const ExtractTextPlugin = require("extract-text-webpack-plugin");

/**
 * Compiles and live reloads page during development.
 * DO NOT LOAD INTO PRODUCTION CONFIGURATION.
 * @param host Specify a host to use. By default this is localhost.
 * @param port Specify a port number to listen for requests on
 * @param proxy Proxying some URLs can be useful when you have a separate API backend development server and you want to send API requests on the same domain.
 * @returns {{devServer: {historyApiFallback: boolean, stats: string, host, port, proxy, overlay: {errors: boolean, warnings: boolean}}}}
 */
exports.devServer = ({ host, port, proxy } = {}) => ({
  devServer: {
    historyApiFallback: true,
    stats: "errors-only",
    host, // Defaults to `localhost`
    port, // Defaults to 8080,
    proxy,
    hot: true,
    overlay: {
      errors: true,
      warnings: true
    }
  },
  plugins: [new webpack.HotModuleReplacementPlugin()]
});

/**
 * Compile ES6+ code to current browser compatible JavaScript
 * @returns {{module: {rules: *[]}}}
 */
exports.loadJavaScript = () => ({
  module: {
    rules: [
      {
        test: /\.js$/,
        loader: "babel-loader",
        exclude: /node_modules/,
        options: {
          // Enable caching for improved performance during
          // development.
          // It uses default OS directory by default. If you need
          // something more custom, pass a path to it.
          // I.e., { cacheDirectory: '<path>' }
          cacheDirectory: true
        }
      }
    ]
  }
});

/**
 * Enforce eslint standard are enforced.
 * @returns {{module: {rules: *[]}}}
 */
exports.lintJavaScript = () => ({
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "eslint-loader"
      }
    ]
  }
});

/**
 * Compress JS.  Make sure that to use angular injections.
 * @returns {{plugins: *[]}}
 */
exports.compressJavaScript = () => {
  console.log(process.env);
  if (process.env.MIN_JS !== "false") {
    return { plugins: [new UglifyJsPlugin({ cache: true, parallel: true })] };
  }
};

const extractSass = new ExtractTextPlugin({
  filename: "css/[name].bundle.css"
  // disable: process.env.NODE_ENV === "development"
});

/**
 * style-loader: Adds CSS to the DOM by injecting a <style> tag
 * css-loader: interprets @import and url() like import/require() and will resolve them.
 * @returns {{module: {rules: *[]}}}
 */
exports.loadCSS = () => ({
  module: {
    rules: [
      {
        test: /\.(s?)css$/,
        use: extractSass.extract({
          use: [
            {
              loader: "css-loader"
            },
            {
              loader: "sass-loader"
            }
          ],
          // use style-loader in development
          fallback: "style-loader"
        })
      }
    ]
  },
  plugins: [extractSass]
});

/**
 * Forces webpack-dev-server program to write bundle files to the file system.
 * @returns {{plugins: *[]}}
 */
exports.writeFilePlugin = () => ({
  plugins: [
    new WriteFilePlugin({
      log: false,
      test: /\.(css|js)/
    })
  ]
});

/**
 * Remove directories listed in the paths
 * @param {list} paths to remove
 * @param {object} options
 *    - verbose: write logs {boolean}
 *    - dry: actually remove the files {boolean}
 *    - allowExternals: Allow the plugin to clean folders outside of the webpack
 *      root. {boolean}
 * @returns {{plugins: *[]}}
 */
exports.clean = (
  paths = [],
  options = {
    verbose: false,
    dry: false,
    allowExternal: true
  }
) => ({
  plugins: [new CleanWebpackPlugin(paths, options)]
});

/**
 * Display a progress bar for the webpack build
 * @returns {{plugins: *[]}}
 */
exports.progressBar = () => ({
  plugins: [new ProgressBarPlugin()]
});
<commit_msg>Removed logging statement.<commit_after>const webpack = require("webpack");
const CleanWebpackPlugin = require("clean-webpack-plugin");
const ProgressBarPlugin = require("progress-bar-webpack-plugin");
const WriteFilePlugin = require("write-file-webpack-plugin");
const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
const ExtractTextPlugin = require("extract-text-webpack-plugin");

/**
 * Compiles and live reloads page during development.
 * DO NOT LOAD INTO PRODUCTION CONFIGURATION.
 * @param host Specify a host to use. By default this is localhost.
 * @param port Specify a port number to listen for requests on
 * @param proxy Proxying some URLs can be useful when you have a separate API backend development server and you want to send API requests on the same domain.
 * @returns {{devServer: {historyApiFallback: boolean, stats: string, host, port, proxy, overlay: {errors: boolean, warnings: boolean}}}}
 */
exports.devServer = ({ host, port, proxy } = {}) => ({
  devServer: {
    historyApiFallback: true,
    stats: "errors-only",
    host, // Defaults to `localhost`
    port, // Defaults to 8080,
    proxy,
    hot: true,
    overlay: {
      errors: true,
      warnings: true
    }
  },
  plugins: [new webpack.HotModuleReplacementPlugin()]
});

/**
 * Compile ES6+ code to current browser compatible JavaScript
 * @returns {{module: {rules: *[]}}}
 */
exports.loadJavaScript = () => ({
  module: {
    rules: [
      {
        test: /\.js$/,
        loader: "babel-loader",
        exclude: /node_modules/,
        options: {
          // Enable caching for improved performance during
          // development.
          // It uses default OS directory by default. If you need
          // something more custom, pass a path to it.
          // I.e., { cacheDirectory: '<path>' }
          cacheDirectory: true
        }
      }
    ]
  }
});

/**
 * Enforce eslint standard are enforced.
 * @returns {{module: {rules: *[]}}}
 */
exports.lintJavaScript = () => ({
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "eslint-loader"
      }
    ]
  }
});

/**
 * Compress JS.  Make sure that to use angular injections.
 * @returns {{plugins: *[]}}
 */
exports.compressJavaScript = () => {
  if (process.env.MIN_JS !== "false") {
    return { plugins: [new UglifyJsPlugin({ cache: true, parallel: true })] };
  }
};

const extractSass = new ExtractTextPlugin({
  filename: "css/[name].bundle.css"
  // disable: process.env.NODE_ENV === "development"
});

/**
 * style-loader: Adds CSS to the DOM by injecting a <style> tag
 * css-loader: interprets @import and url() like import/require() and will resolve them.
 * @returns {{module: {rules: *[]}}}
 */
exports.loadCSS = () => ({
  module: {
    rules: [
      {
        test: /\.(s?)css$/,
        use: extractSass.extract({
          use: [
            {
              loader: "css-loader"
            },
            {
              loader: "sass-loader"
            }
          ],
          // use style-loader in development
          fallback: "style-loader"
        })
      }
    ]
  },
  plugins: [extractSass]
});

/**
 * Forces webpack-dev-server program to write bundle files to the file system.
 * @returns {{plugins: *[]}}
 */
exports.writeFilePlugin = () => ({
  plugins: [
    new WriteFilePlugin({
      log: false,
      test: /\.(css|js)/
    })
  ]
});

/**
 * Remove directories listed in the paths
 * @param {list} paths to remove
 * @param {object} options
 *    - verbose: write logs {boolean}
 *    - dry: actually remove the files {boolean}
 *    - allowExternals: Allow the plugin to clean folders outside of the webpack
 *      root. {boolean}
 * @returns {{plugins: *[]}}
 */
exports.clean = (
  paths = [],
  options = {
    verbose: false,
    dry: false,
    allowExternal: true
  }
) => ({
  plugins: [new CleanWebpackPlugin(paths, options)]
});

/**
 * Display a progress bar for the webpack build
 * @returns {{plugins: *[]}}
 */
exports.progressBar = () => ({
  plugins: [new ProgressBarPlugin()]
});
<|endoftext|>


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

var path  = require('path');
var fs    = require('fs');
var ytdl  = require('..');
var cliff = require('cliff');
require('colors');

var info = JSON.parse(
    fs.readFileSync(path.resolve(__dirname, '..', 'package.json')));

var opts = require('nomnom')
  .option('version', {
    abbr: 'v'
  , flag: true
  , callback: function() {
      console.log('v' + info.version);
      process.exit();
    }
  , help: 'Print program version.'
  })
  .option('url', {
    position: 0
  , required: true
  , help: 'URL to the video.'
  })
  .option('quality', {
    abbr: 'q'
  , metavar: 'ITAG'
  , help: 'Video quality to download. Default: `highest`'
  })
  .option('start', {
    abbr: 's'
  , metavar: 'TIME'
  , help: 'Where to begin the video. ie 1m3s, 45s, 2300.'
  })
  .option('output', {
    abbr: 'o'
  , metavar: 'FILE'
  , help: 'Where to save the file. Default: stdout'
  })
  .option('filterContainer', {
    full: 'filter-container'
  , metavar: 'REGEXP'
  , help: 'Filter in format container.'
  })
  .option('unfilterContainer', {
    full: 'unfilter-container'
  , metavar: 'REGEXP'
  , help: 'Filter out format container.'
  })
  .option('filterResolution', {
    full: 'filter-resolution'
  , metavar: 'REGEXP'
  , help: 'Filter in format resolution.'
  })
  .option('unfilterResolution', {
    full: 'unfilter-resolution'
  , metavar: 'REGEXP'
  , help: 'Filter out format resolution.'
  })
  .option('filterEncoding', {
    full: 'filter-encoding'
  , metavar: 'REGEXP'
  , help: 'Filter in format encoding.'
  })
  .option('unfilterEncoding', {
    full: 'unfilter-encoding'
  , metavar: 'REGEXP'
  , help: 'Filter out format encoding.'
  })
  .option('info', {
    abbr: 'i'
  , flag: true
  , help: 'Print only video information without downloading'
  })
  .script('ytdl')
  .colors()
  .parse()
  ;

/**
 * Converts seconds into human readable time hh:mm:ss
 *
 * @param {Number} seconds
 * @return {String}
 */
function toHumanTime(seconds) {
  var h = Math.floor(seconds / 3600);
  if (h < 10) { h = '0' + h; }
  var m = Math.floor(seconds / 60) % 60;
  if (m < 10) { m = '0' + m; }
  var s = seconds % 60;
  if (s < 10) { s = '0' + s; }
  return h + ':' + m + ':' + s;
}

/**
 * Prints basic video information.
 *
 * @param {Object} info
 */
function printVideoInfo(info) {
  console.log();
  console.log('title: '.grey.bold + info.title);
  console.log('author: '.grey.bold + info.author);
  var rating = typeof info.avg_rating === 'number' ?
    info.avg_rating.toFixed(1) : info.avg_rating;
  console.log('average rating: '.grey.bold + rating);
  console.log('view count: '.grey.bold + info.view_count);
  console.log('length: '.grey.bold + toHumanTime(info.length_seconds));
}

if (opts.info) {
  ytdl.getInfo(opts.url, function(err, info) {
    if (err) {
      console.error(err.message);
      process.exit(1);
      return;
    }

    printVideoInfo(info);

    console.log('formats:'.grey.bold);
    var cols = ['itag', 'container', 'resolution', 'encoding'];
    var colors = ['green', 'blue', 'green', 'blue'];
    console.log(cliff.stringifyObjectRows(info.formats, cols, colors));
    ytdl.cache.die();
  });
  return;
}

var output = opts.output;
var writeStream = output ? fs.createWriteStream(output) : process.stdout;

var ytdlOptions = {};
ytdlOptions.quality = opts.quality;
ytdlOptions.begin = opts.begin;

// Create filters.
var filters = [];

/**
 * @param {String} field
 * @param {String} regexpStr
 * @param {Boolean|null} negated
 */
function createFilter(field, regexpStr, negated) {
  try {
    var regexp = new RegExp(regexpStr, 'i');
  } catch (err) {
    console.error(err.message);
    process.exit(1);
  }

  filters.push(function(format) {
    return negated !== regexp.test(format[field]);
  });
}

['container', 'resolution', 'encoding'].forEach(function(field) {
  var key = 'filter' + field[0].toUpperCase() + field.slice(1);
  if (opts[key]) {
    createFilter(field, opts[key], false);
  }

  key = 'un' + key;
  if (opts[key]) {
    createFilter(field, opts[key], true);
  }
});

ytdlOptions.filter = function(format) {
  return filters.every(function(filter) {
    return filter(format);
  });
};

var readStream = ytdl(opts.url, ytdlOptions);
readStream.pipe(writeStream);

readStream.on('error', function(err) {
  console.error(err.stack);
  process.exit(1);
});

// Converst bytes to human readable unit.
// Thank you Amir from StackOverflow.
var units = ' KMGTPEZYXWVU';
function toHumanSize(bytes) {
  if (bytes <= 0) { return 0; }
  var t2 = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), 12);
  return (Math.round(bytes * 100 / Math.pow(1024, t2)) / 100) +
          units.charAt(t2).replace(' ', '') + 'B';
}

// Print progress bar and some video info if not streaming to stdout.
if (output) {
  readStream.on('info', function(info, format) {
    printVideoInfo(info);
    console.log('container: '.grey.bold + format.container);
    console.log('resolution: '.grey.bold + format.resolution);
    console.log('encoding: '.grey.bold + format.encoding);
    console.log('size: '.grey.bold + toHumanSize(format.size));
    console.log('output: '.grey.bold + output);
    console.log();

    // Create progress bar.
    var bar = require('progress-bar').create(process.stdout, 50);
    bar.format = '$bar; $percentage;%';

    // Keep track of progress.
    var dataRead = 0;
    readStream.on('data', function(data) {
      dataRead += data.length;
      var percent = dataRead / format.size;
      bar.update(percent);
    });

    ytdl.cache.die();

  });
}

readStream.on('end', function onend() {
  console.log();
});

process.on('SIGINT', function onsigint() {
  console.log();
  process.exit();
});
<commit_msg>[fix] use correct option name<commit_after>#!/usr/bin/env node

var path  = require('path');
var fs    = require('fs');
var ytdl  = require('..');
var cliff = require('cliff');
require('colors');

var info = JSON.parse(
    fs.readFileSync(path.resolve(__dirname, '..', 'package.json')));

var opts = require('nomnom')
  .option('version', {
    abbr: 'v'
  , flag: true
  , callback: function() {
      console.log('v' + info.version);
      process.exit();
    }
  , help: 'Print program version.'
  })
  .option('url', {
    position: 0
  , required: true
  , help: 'URL to the video.'
  })
  .option('quality', {
    abbr: 'q'
  , metavar: 'ITAG'
  , help: 'Video quality to download. Default: `highest`'
  })
  .option('start', {
    abbr: 's'
  , metavar: 'TIME'
  , help: 'Where to begin the video. ie 1m3s, 45s, 2300.'
  })
  .option('output', {
    abbr: 'o'
  , metavar: 'FILE'
  , help: 'Where to save the file. Default: stdout'
  })
  .option('filterContainer', {
    full: 'filter-container'
  , metavar: 'REGEXP'
  , help: 'Filter in format container.'
  })
  .option('unfilterContainer', {
    full: 'unfilter-container'
  , metavar: 'REGEXP'
  , help: 'Filter out format container.'
  })
  .option('filterResolution', {
    full: 'filter-resolution'
  , metavar: 'REGEXP'
  , help: 'Filter in format resolution.'
  })
  .option('unfilterResolution', {
    full: 'unfilter-resolution'
  , metavar: 'REGEXP'
  , help: 'Filter out format resolution.'
  })
  .option('filterEncoding', {
    full: 'filter-encoding'
  , metavar: 'REGEXP'
  , help: 'Filter in format encoding.'
  })
  .option('unfilterEncoding', {
    full: 'unfilter-encoding'
  , metavar: 'REGEXP'
  , help: 'Filter out format encoding.'
  })
  .option('info', {
    abbr: 'i'
  , flag: true
  , help: 'Print only video information without downloading'
  })
  .script('ytdl')
  .colors()
  .parse()
  ;

/**
 * Converts seconds into human readable time hh:mm:ss
 *
 * @param {Number} seconds
 * @return {String}
 */
function toHumanTime(seconds) {
  var h = Math.floor(seconds / 3600);
  if (h < 10) { h = '0' + h; }
  var m = Math.floor(seconds / 60) % 60;
  if (m < 10) { m = '0' + m; }
  var s = seconds % 60;
  if (s < 10) { s = '0' + s; }
  return h + ':' + m + ':' + s;
}

/**
 * Prints basic video information.
 *
 * @param {Object} info
 */
function printVideoInfo(info) {
  console.log();
  console.log('title: '.grey.bold + info.title);
  console.log('author: '.grey.bold + info.author);
  var rating = typeof info.avg_rating === 'number' ?
    info.avg_rating.toFixed(1) : info.avg_rating;
  console.log('average rating: '.grey.bold + rating);
  console.log('view count: '.grey.bold + info.view_count);
  console.log('length: '.grey.bold + toHumanTime(info.length_seconds));
}

if (opts.info) {
  ytdl.getInfo(opts.url, function(err, info) {
    if (err) {
      console.error(err.message);
      process.exit(1);
      return;
    }

    printVideoInfo(info);

    console.log('formats:'.grey.bold);
    var cols = ['itag', 'container', 'resolution', 'encoding'];
    var colors = ['green', 'blue', 'green', 'blue'];
    console.log(cliff.stringifyObjectRows(info.formats, cols, colors));
    ytdl.cache.die();
  });
  return;
}

var output = opts.output;
var writeStream = output ? fs.createWriteStream(output) : process.stdout;

var ytdlOptions = {};
ytdlOptions.quality = opts.quality;
ytdlOptions.start = opts.start;

// Create filters.
var filters = [];

/**
 * @param {String} field
 * @param {String} regexpStr
 * @param {Boolean|null} negated
 */
function createFilter(field, regexpStr, negated) {
  try {
    var regexp = new RegExp(regexpStr, 'i');
  } catch (err) {
    console.error(err.message);
    process.exit(1);
  }

  filters.push(function(format) {
    return negated !== regexp.test(format[field]);
  });
}

['container', 'resolution', 'encoding'].forEach(function(field) {
  var key = 'filter' + field[0].toUpperCase() + field.slice(1);
  if (opts[key]) {
    createFilter(field, opts[key], false);
  }

  key = 'un' + key;
  if (opts[key]) {
    createFilter(field, opts[key], true);
  }
});

ytdlOptions.filter = function(format) {
  return filters.every(function(filter) {
    return filter(format);
  });
};

var readStream = ytdl(opts.url, ytdlOptions);
readStream.pipe(writeStream);

readStream.on('error', function(err) {
  console.error(err.stack);
  process.exit(1);
});

// Converst bytes to human readable unit.
// Thank you Amir from StackOverflow.
var units = ' KMGTPEZYXWVU';
function toHumanSize(bytes) {
  if (bytes <= 0) { return 0; }
  var t2 = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), 12);
  return (Math.round(bytes * 100 / Math.pow(1024, t2)) / 100) +
          units.charAt(t2).replace(' ', '') + 'B';
}

// Print progress bar and some video info if not streaming to stdout.
if (output) {
  readStream.on('info', function(info, format) {
    printVideoInfo(info);
    console.log('container: '.grey.bold + format.container);
    console.log('resolution: '.grey.bold + format.resolution);
    console.log('encoding: '.grey.bold + format.encoding);
    console.log('size: '.grey.bold + toHumanSize(format.size));
    console.log('output: '.grey.bold + output);
    console.log();

    // Create progress bar.
    var bar = require('progress-bar').create(process.stdout, 50);
    bar.format = '$bar; $percentage;%';

    // Keep track of progress.
    var dataRead = 0;
    readStream.on('data', function(data) {
      dataRead += data.length;
      var percent = dataRead / format.size;
      bar.update(percent);
    });

    ytdl.cache.die();

  });
}

readStream.on('end', function onend() {
  console.log();
});

process.on('SIGINT', function onsigint() {
  console.log();
  process.exit();
});
<|endoftext|>


<commit_before>var username = "";
var room = "";
var socket = io.connect();
var title = "";

$("#changeRoom").modal({
	keyboard: false,
	backdrop: "static"
});

$("#changeTitle").modal({
	keyboard: false,
	backdrop: "static"
});
$("#changeTitle").modal("hide");

$("#changeRoom").modal("show");
$("#urlAlert").hide();
$("#changeRoomFormAlert").hide();

$("#changeRoomButton").click(function()
{
	location.reload();
});

$("#joinRoomButton").click(function()
{
	tmpUsername = $("#usernameField").val();
	tmpRoom = parseInt($("#roomNumberField").val());
	if ( tmpUsername != "" && tmpRoom )
	{
		$("#chatBody").html("");
		username = tmpUsername
		room = tmpRoom;
		$("#roomNumber").html(room);
		$("#changeRoom").modal("hide");
		$("#changeRoomFormAlert").hide();
		socket.emit("resync");
	}
	else
	{
		if ( !tmpRoom )
			$("#changeRoomFormAlert").html("Please enter a number in the room field.");
		if ( tmpUsername == "" )
			$("#changeRoomFormAlert").html("Please fill out the name field.");
		$("#changeRoomFormAlert").show();
	}
});

$("#urlSubmitButton").click(function()
{
	url = $("#urlInputField").val();
	prefix = url.split(":")[0];
	if ( prefix != "http" && prefix != "https" )
	{
		$("#urlAlert").show();
		return;
	}
	socket.emit("link", {room:room, username:username, linkTitle:url, url:url, customTitle:title});
	$("#urlInfoButton").html("Set Title");
	$("#fileInfoButton").html("Set Title");
	title = '';
	$("#urlInputField").val("");
	$("#urlAlert").hide();
	barResize();
});

$("#urlInfoButton").click(function()
{
	$("#changeTitle").modal("show");
});

$("#fileInfoButton").click(function()
{
	$("#changeTitle").modal("show");
});

$("#setTitleButton").click(function()
{
	title = $.trim($("#urlTitleField").val());
	$("#urlTitleField").val("");
	$("#fileTitleField").val("");

	if ( title == "" ){
		$("#urlInfoButton").html("Set Title");
		$("#fileInfoButton").html("Set Title");
	}
	else{
		$("#urlInfoButton").html(title);
		$("#fileInfoButton").html(title);
	}
	$("#changeTitle").modal("hide");
	barResize();
});

$("#changeTitleBackButton").click(function()
{
	$("#changeTitle").modal("hide");
});

$('#urlInputField').keypress(function(e)
{
    if(e.which == 13) {
		url = $("#urlInputField").val();
		prefix = url.split(":")[0];
		if ( prefix != "http" && prefix != "https" )
		{
			$("#urlAlert").show();
			return;
		}
		$("#urlInfoButton").html("Set Title");
		$("#fileInfoButton").html("Set Title");
		title = '';
		socket.emit("link", {room:room, username:username, linkTitle:url, url:url, customTitle:title});
		$("#urlInputField").val("");
		$("#urlAlert").hide();
    }
});

function appendLink(user, urlName, url, customTitle)
{
	if ( customTitle == "" ){
		data = createElement('p');
		insertElementAt(createElement('strong', null, user), data);
		insertElementAt(createText(' posted a link to '), data);
		insertElementAt(createElement('a', {'href':url}, urlName), data);
		$("#chatBody").prepend(data)
	}
	else{
		data = createElement('p');
		insertElementAt(createElement('strong', null, user), data);
		insertElementAt(createText(" posted a link for " + customTitle + " - "), data);
		insertElementAt(createElement('a', {'href':url}, urlName), data);
		$("#chatBody").prepend(data)
	}
}

function barResize(){
	var barSize = $('#chatBody').outerWidth();
	var setUrlButtonSize = $('#urlInfoButton').outerWidth();
	var sendUrlButtonSize = $('#urlSubmitButton').outerWidth();
	var setFileButtonSize = $('#fileInfoButton').outerWidth();
	var sendFileButtonSize = $('#fileSubmitButton').outerWidth();

	$('#urlInputField').outerWidth(barSize-setUrlButtonSize-sendUrlButtonSize);
	if(document.getElementById("fileInput").files.length == 0)
		$('#fileInputField').outerWidth(barSize-setFileButtonSize-sendFileButtonSize - $('#fileUploadButton').innerWidth());
	else
		$('#fileInputField').innerWidth(barSize-setFileButtonSize-sendFileButtonSize - $('#fileUploadButton').innerWidth() - $('#fileRemoveButton').innerWidth());

}
$(document).ready(function() {
  barResize();
});

$(window).resize(barResize);

$('a[data-toggle="tab"]').on('shown', function (e) {
	barResize();
});

$('#fileInput').change(function (evt){
	barResize();
});

$("#fileSubmitButton").click(function()
{
	var fileObj = document.getElementById("fileInput");
	if ( fileObj.files.length == 0 )
		return;

	var f = new FormData();
	f.append("file", fileObj.files[0]);
	f.append("room", room);
	f.append("username", username);
	f.append("customTitle", title);

	$("#urlInfoButton").html("Set Title");
	$("#fileInfoButton").html("Set Title");
	title = '';

	$.ajax({
		"url": "/uploadHandler",
		data: f,
		contentType: false,
		processData: false,
		type: "POST",
		success: function ()
		{
			$('.fileupload').fileupload('reset');
			barResize();
		}
	});

});

socket.on("link", function(data)
{
	if ( data.room == room )
		appendLink(data.username, data.linkTitle, data.url, data.customTitle);
});

socket.on("file", function(data)
{
	if ( data.room == room )
	{
		ele = createElement('p');
		insertElementAt(createElement('strong', null, data.username), ele);
		insertElementAt(createText(' posted a file: '), ele);
		insertElementAt(createElement('a', {'href':"uploads/"+data.fsFileName}, data.title), ele);
		$("#chatBody").prepend(ele);
	}
});<commit_msg>Fixed custom title uploads<commit_after>var username = "";
var room = "";
var socket = io.connect();
var title = "";

$("#changeRoom").modal({
	keyboard: false,
	backdrop: "static"
});

$("#changeTitle").modal({
	keyboard: false,
	backdrop: "static"
});
$("#changeTitle").modal("hide");

$("#changeRoom").modal("show");
$("#urlAlert").hide();
$("#changeRoomFormAlert").hide();

$("#changeRoomButton").click(function()
{
	location.reload();
});

$("#joinRoomButton").click(function()
{
	tmpUsername = $("#usernameField").val();
	tmpRoom = parseInt($("#roomNumberField").val());
	if ( tmpUsername != "" && tmpRoom )
	{
		$("#chatBody").html("");
		username = tmpUsername
		room = tmpRoom;
		$("#roomNumber").html(room);
		$("#changeRoom").modal("hide");
		$("#changeRoomFormAlert").hide();
		socket.emit("resync");
	}
	else
	{
		if ( !tmpRoom )
			$("#changeRoomFormAlert").html("Please enter a number in the room field.");
		if ( tmpUsername == "" )
			$("#changeRoomFormAlert").html("Please fill out the name field.");
		$("#changeRoomFormAlert").show();
	}
});

$("#urlSubmitButton").click(function()
{
	url = $("#urlInputField").val();
	prefix = url.split(":")[0];
	if ( prefix != "http" && prefix != "https" )
	{
		$("#urlAlert").show();
		return;
	}
	socket.emit("link", {room:room, username:username, linkTitle:url, url:url, customTitle:title});
	$("#urlInfoButton").html("Set Title");
	$("#fileInfoButton").html("Set Title");
	title = '';
	$("#urlInputField").val("");
	$("#urlAlert").hide();
	barResize();
});

$("#urlInfoButton").click(function()
{
	$("#changeTitle").modal("show");
});

$("#fileInfoButton").click(function()
{
	$("#changeTitle").modal("show");
});

$("#setTitleButton").click(function()
{
	title = $.trim($("#urlTitleField").val());
	$("#urlTitleField").val("");
	$("#fileTitleField").val("");

	if ( title == "" ){
		$("#urlInfoButton").html("Set Title");
		$("#fileInfoButton").html("Set Title");
	}
	else{
		$("#urlInfoButton").html(title);
		$("#fileInfoButton").html(title);
	}
	$("#changeTitle").modal("hide");
	barResize();
});

$("#changeTitleBackButton").click(function()
{
	$("#changeTitle").modal("hide");
});

$('#urlInputField').keypress(function(e)
{
    if(e.which == 13) {
		url = $("#urlInputField").val();
		prefix = url.split(":")[0];
		if ( prefix != "http" && prefix != "https" )
		{
			$("#urlAlert").show();
			return;
		}
		$("#urlInfoButton").html("Set Title");
		$("#fileInfoButton").html("Set Title");
		title = '';
		socket.emit("link", {room:room, username:username, linkTitle:url, url:url, customTitle:title});
		$("#urlInputField").val("");
		$("#urlAlert").hide();
    }
});

function appendLink(user, urlName, url, customTitle)
{
	if ( customTitle == "" ){
		data = createElement('p');
		insertElementAt(createElement('strong', null, user), data);
		insertElementAt(createText(' posted a link to '), data);
		insertElementAt(createElement('a', {'href':url}, urlName), data);
		$("#chatBody").prepend(data)
	}
	else{
		data = createElement('p');
		insertElementAt(createElement('strong', null, user), data);
		insertElementAt(createText(" posted a link for " + customTitle + " - "), data);
		insertElementAt(createElement('a', {'href':url}, urlName), data);
		$("#chatBody").prepend(data)
	}
}

function barResize(){
	var barSize = $('#chatBody').outerWidth();
	var setUrlButtonSize = $('#urlInfoButton').outerWidth();
	var sendUrlButtonSize = $('#urlSubmitButton').outerWidth();
	var setFileButtonSize = $('#fileInfoButton').outerWidth();
	var sendFileButtonSize = $('#fileSubmitButton').outerWidth();

	$('#urlInputField').outerWidth(barSize-setUrlButtonSize-sendUrlButtonSize);
	if(document.getElementById("fileInput").files.length == 0)
		$('#fileInputField').outerWidth(barSize-setFileButtonSize-sendFileButtonSize - $('#fileUploadButton').innerWidth());
	else
		$('#fileInputField').innerWidth(barSize-setFileButtonSize-sendFileButtonSize - $('#fileUploadButton').innerWidth() - $('#fileRemoveButton').innerWidth());

}
$(document).ready(function() {
  barResize();
});

$(window).resize(barResize);

$('a[data-toggle="tab"]').on('shown', function (e) {
	barResize();
});

$('#fileInput').change(function (evt){
	barResize();
});

$("#fileSubmitButton").click(function()
{
	var fileObj = document.getElementById("fileInput");
	if ( fileObj.files.length == 0 )
		return;

	var f = new FormData();
	f.append("file", fileObj.files[0]);
	f.append("room", room);
	f.append("username", username);
	f.append("title", title);

	$("#urlInfoButton").html("Set Title");
	$("#fileInfoButton").html("Set Title");
	title = '';

	$.ajax({
		"url": "/uploadHandler",
		data: f,
		contentType: false,
		processData: false,
		type: "POST",
		success: function ()
		{
			$('.fileupload').fileupload('reset');
			barResize();
		}
	});

});

socket.on("link", function(data)
{
	if ( data.room == room )
		appendLink(data.username, data.linkTitle, data.url, data.customTitle);
});

socket.on("file", function(data)
{
	if ( data.room == room )
	{
		ele = createElement('p');
		insertElementAt(createElement('strong', null, data.username), ele);
		insertElementAt(createText(' posted a file: '), ele);
		insertElementAt(createElement('a', {'href':"uploads/"+data.fsFileName}, data.title), ele);
		$("#chatBody").prepend(ele);
	}
});<|endoftext|>


<commit_before>/* global $, APP, interfaceConfig */
/* jshint -W101 */

import UIUtil from "../util/UIUtil";
import UIEvents from "../../../service/UI/UIEvents";
import LargeContainer from './LargeContainer';
import BottomToolbar from '../toolbars/BottomToolbar';
import Avatar from "../avatar/Avatar";
import {createDeferred} from '../../util/helpers';

const avatarSize = interfaceConfig.DOMINANT_SPEAKER_AVATAR_SIZE;
const FADE_DURATION_MS = 300;

/**
 * Get stream id.
 * @param {JitsiTrack?} stream
 */
function getStreamOwnerId(stream) {
    if (!stream) {
        return;
    }
    if (stream.isLocal()) { // local stream doesn't have method "getParticipantId"
        return APP.conference.localId;
    } else {
        return stream.getParticipantId();
    }
}

/**
 * Returns an array of the video dimensions, so that it keeps it's aspect
 * ratio and fits available area with it's larger dimension. This method
 * ensures that whole video will be visible and can leave empty areas.
 *
 * @return an array with 2 elements, the video width and the video height
 */
function getDesktopVideoSize(videoWidth,
                             videoHeight,
                             videoSpaceWidth,
                             videoSpaceHeight) {

    let aspectRatio = videoWidth / videoHeight;

    let availableWidth = Math.max(videoWidth, videoSpaceWidth);
    let availableHeight = Math.max(videoHeight, videoSpaceHeight);

    videoSpaceHeight -= BottomToolbar.getFilmStripHeight();

    if (availableWidth / aspectRatio >= videoSpaceHeight) {
        availableHeight = videoSpaceHeight;
        availableWidth = availableHeight * aspectRatio;
    }

    if (availableHeight * aspectRatio >= videoSpaceWidth) {
        availableWidth = videoSpaceWidth;
        availableHeight = availableWidth / aspectRatio;
    }

    return [ availableWidth, availableHeight ];
}

/**
 * Returns an array of the video dimensions. It respects the
 * VIDEO_LAYOUT_FIT config, to fit the video to the screen, by hiding some parts
 * of it, or to fit it to the height or width.
 *
 * @param videoWidth the original video width
 * @param videoHeight the original video height
 * @param videoSpaceWidth the width of the video space
 * @param videoSpaceHeight the height of the video space
 * @return an array with 2 elements, the video width and the video height
 */
function getCameraVideoSize(videoWidth,
                            videoHeight,
                            videoSpaceWidth,
                            videoSpaceHeight) {

    let aspectRatio = videoWidth / videoHeight;

    let availableWidth = videoWidth;
    let availableHeight = videoHeight;

    if (interfaceConfig.VIDEO_LAYOUT_FIT == 'height') {
        availableHeight = videoSpaceHeight;
        availableWidth = availableHeight*aspectRatio;
    }
    else if (interfaceConfig.VIDEO_LAYOUT_FIT == 'width') {
        availableWidth = videoSpaceWidth;
        availableHeight = availableWidth/aspectRatio;
    }
    else if (interfaceConfig.VIDEO_LAYOUT_FIT == 'both') {
        availableWidth = Math.max(videoWidth, videoSpaceWidth);
        availableHeight = Math.max(videoHeight, videoSpaceHeight);

        if (availableWidth / aspectRatio < videoSpaceHeight) {
            availableHeight = videoSpaceHeight;
            availableWidth = availableHeight * aspectRatio;
        }

        if (availableHeight * aspectRatio < videoSpaceWidth) {
            availableWidth = videoSpaceWidth;
            availableHeight = availableWidth / aspectRatio;
        }
    }

    return [ availableWidth, availableHeight ];
}

/**
 * Returns an array of the video horizontal and vertical indents,
 * so that if fits its parent.
 *
 * @return an array with 2 elements, the horizontal indent and the vertical
 * indent
 */
function getCameraVideoPosition(videoWidth,
                                videoHeight,
                                videoSpaceWidth,
                                videoSpaceHeight) {
    // Parent height isn't completely calculated when we position the video in
    // full screen mode and this is why we use the screen height in this case.
    // Need to think it further at some point and implement it properly.
    if (UIUtil.isFullScreen()) {
        videoSpaceHeight = window.innerHeight;
    }

    let horizontalIndent = (videoSpaceWidth - videoWidth) / 2;
    let verticalIndent = (videoSpaceHeight - videoHeight) / 2;

    return { horizontalIndent, verticalIndent };
}

/**
 * Returns an array of the video horizontal and vertical indents.
 * Centers horizontally and top aligns vertically.
 *
 * @return an array with 2 elements, the horizontal indent and the vertical
 * indent
 */
function getDesktopVideoPosition(videoWidth,
                                 videoHeight,
                                 videoSpaceWidth,
                                 videoSpaceHeight) {

    let horizontalIndent = (videoSpaceWidth - videoWidth) / 2;

    let verticalIndent = 0;// Top aligned

    return { horizontalIndent, verticalIndent };
}

export const VideoContainerType = "video";

/**
 * Container for user video.
 */
class VideoContainer extends LargeContainer {
    // FIXME: With Temasys we have to re-select everytime
    get $video () {
        return $('#largeVideo');
    }

    get id () {
        return getStreamOwnerId(this.stream);
    }

    constructor (onPlay) {
        super();
        this.stream = null;
        this.videoType = null;

        this.isVisible = false;

        this.$avatar = $('#dominantSpeaker');
        this.$wrapper = $('#largeVideoWrapper');

        // This does not work with Temasys plugin - has to be a property to be
        // copied between new <object> elements
        //this.$video.on('play', onPlay);
        this.$video[0].onplay = onPlay;
    }

    /**
     * Get size of video element.
     * @returns {{width, height}}
     */
    getStreamSize () {
        let video = this.$video[0];
        return {
            width: video.videoWidth,
            height: video.videoHeight
        };
    }

    /**
     * Calculate optimal video size for specified container size.
     * @param {number} containerWidth container width
     * @param {number} containerHeight container height
     * @returns {{availableWidth, availableHeight}}
     */
    getVideoSize (containerWidth, containerHeight) {
        let { width, height } = this.getStreamSize();
        if (this.stream && this.isScreenSharing()) {
            return getDesktopVideoSize( width,
                                        height,
                                        containerWidth,
                                        containerHeight);
        } else {
            return getCameraVideoSize(  width,
                                        height,
                                        containerWidth,
                                        containerHeight);
        }
    }

    /**
     * Calculate optimal video position (offset for top left corner)
     * for specified video size and container size.
     * @param {number} width video width
     * @param {number} height video height
     * @param {number} containerWidth container width
     * @param {number} containerHeight container height
     * @returns {{horizontalIndent, verticalIndent}}
     */
    getVideoPosition (width, height, containerWidth, containerHeight) {
        if (this.stream && this.isScreenSharing()) {
            return getDesktopVideoPosition( width,
                                            height,
                                            containerWidth,
                                            containerHeight);
        } else {
            return getCameraVideoPosition(  width,
                                            height,
                                            containerWidth,
                                            containerHeight);
        }
    }

    resize (containerWidth, containerHeight, animate = false) {
        let [width, height]
            = this.getVideoSize(containerWidth, containerHeight);
        let { horizontalIndent, verticalIndent }
            = this.getVideoPosition(width, height,
                                    containerWidth, containerHeight);

        // update avatar position
        let top = containerHeight / 2 - avatarSize / 4 * 3;

        this.$avatar.css('top', top);

        this.$wrapper.animate({
            width: width,
            height: height,

            top: verticalIndent,
            bottom: verticalIndent,

            left: horizontalIndent,
            right: horizontalIndent
        }, {
            queue: false,
            duration: animate ? 500 : 0
        });
    }

    /**
     * Update video stream.
     * @param {JitsiTrack?} stream new stream
     * @param {string} videoType video type
     */
    setStream (stream, videoType) {
        this.stream = stream;
        this.videoType = videoType;

        if(!stream)
            return;

        stream.attach(this.$video[0]);

        let flipX = stream.isLocal() && !this.isScreenSharing();
        this.$video.css({
            transform: flipX ? 'scaleX(-1)' : 'none'
        });
    }

    /**
     * Check if current video stream is screen sharing.
     * @returns {boolean}
     */
    isScreenSharing () {
        return this.videoType === 'desktop';
    }

    /**
     * Show or hide user avatar.
     * @param {boolean} show
     */
    showAvatar (show) {
        this.$avatar.css("visibility", show ? "visible" : "hidden");
    }

    // We are doing fadeOut/fadeIn animations on parent div which wraps
    // largeVideo, because when Temasys plugin is in use it replaces
    // <video> elements with plugin <object> tag. In Safari jQuery is
    // unable to store values on this plugin object which breaks all
    // animation effects performed on it directly.

    show () {
        // its already visible
        if (this.isVisible) {
            return Promise.resolve();
        }

        let $wrapper = this.$wrapper;
        return new Promise((resolve) => {
            this.$wrapper.css('visibility', 'visible').fadeTo(
                FADE_DURATION_MS,
                1,
                () => {
                    this.isVisible = true;
                    resolve();
                }
            );
        });
    }

    hide () {
        // its already hidden
        if (!this.isVisible) {
            return Promise.resolve();
        }

        return new Promise((resolve) => {
            this.$wrapper.fadeTo(FADE_DURATION_MS, 0, () => {
                this.$wrapper.css('visibility', 'hidden');
                this.isVisible = false;
                resolve();
            });
        });
    }
}

/**
 * Manager for all Large containers.
 */
export default class LargeVideoManager {
    constructor () {
        this.containers = {};

        this.state = VideoContainerType;
        this.videoContainer = new VideoContainer(() => this.resizeContainer(VideoContainerType));
        this.addContainer(VideoContainerType, this.videoContainer);

        this.width = 0;
        this.height = 0;

        this.$container = $('#largeVideoContainer');

        this.$container.css({
            display: 'inline-block'
        });

        if (interfaceConfig.SHOW_JITSI_WATERMARK) {
            let leftWatermarkDiv = this.$container.find("div.watermark.leftwatermark");

            leftWatermarkDiv.css({display: 'block'});

            leftWatermarkDiv.parent().attr('href', interfaceConfig.JITSI_WATERMARK_LINK);
        }

        if (interfaceConfig.SHOW_BRAND_WATERMARK) {
            let rightWatermarkDiv = this.$container.find("div.watermark.rightwatermark");

            rightWatermarkDiv.css({
                display: 'block',
                backgroundImage: 'url(images/rightwatermark.png)'
            });

            rightWatermarkDiv.parent().attr('href', interfaceConfig.BRAND_WATERMARK_LINK);
        }

        if (interfaceConfig.SHOW_POWERED_BY) {
            this.$container.children("a.poweredby").css({display: 'block'});
        }

        this.$container.hover(
            e => this.onHoverIn(e),
            e => this.onHoverOut(e)
        );
    }

    onHoverIn (e) {
        if (!this.state) {
            return;
        }
        let container = this.getContainer(this.state);
        container.onHoverIn(e);
    }

    onHoverOut (e) {
        if (!this.state) {
            return;
        }
        let container = this.getContainer(this.state);
        container.onHoverOut(e);
    }

    get id () {
        return this.videoContainer.id;
    }

    scheduleLargeVideoUpdate () {
        if (this.updateInProcess || !this.newStreamData) {
            return;
        }

        this.updateInProcess = true;

        let container = this.getContainer(this.state);

        container.hide().then(() => {
            let {id, stream, videoType, resolve} = this.newStreamData;
            this.newStreamData = null;

            console.info("hover in %s", id);
            this.state = VideoContainerType;
            this.videoContainer.setStream(stream, videoType);

            // change the avatar url on large
            this.updateAvatar(Avatar.getAvatarUrl(id));

            let isVideoMuted = stream ? stream.isMuted() : true;

            // show the avatar on large if needed
            this.videoContainer.showAvatar(isVideoMuted);

            let promise;

            // do not show stream if video is muted
            // but we still should show watermark
            if (isVideoMuted) {
                this.showWatermark(true);
                promise = Promise.resolve();
            } else {
                promise = this.videoContainer.show();
            }

            // resolve updateLargeVideo promise after everything is done
            promise.then(resolve);

            return promise;
        }).then(() => {
            // after everything is done check again if there are any pending new streams.
            this.updateInProcess = false;
            this.scheduleLargeVideoUpdate();
        });
    }

    /**
     * Update large video.
     * Switches to large video even if previously other container was visible.
     * @param userID the userID of the participant associated with the stream
     * @param {JitsiTrack?} stream new stream
     * @param {string?} videoType new video type
     * @returns {Promise}
     */
    updateLargeVideo (userID, stream, videoType) {
        if (this.newStreamData) {
            this.newStreamData.reject();
        }

        this.newStreamData = createDeferred();
        this.newStreamData.id = userID;
        this.newStreamData.stream = stream;
        this.newStreamData.videoType = videoType;

        this.scheduleLargeVideoUpdate();

        return this.newStreamData.promise;
    }

    /**
     * Update container size optionally taking side bar size into account.
     * @param {boolean} isSideBarVisible if side bar is visible.
     */
    updateContainerSize (isSideBarVisible) {
        this.width = UIUtil.getAvailableVideoWidth(isSideBarVisible);
        this.height = window.innerHeight;
    }

    /**
     * Resize Large container of specified type.
     * @param {string} type type of container which should be resized.
     * @param {boolean} [animate=false] if resize process should be animated.
     */
    resizeContainer (type, animate = false) {
        let container = this.getContainer(type);
        container.resize(this.width, this.height, animate);
    }

    /**
     * Resize all Large containers.
     * @param {boolean} animate if resize process should be animated.
     */
    resize (animate) {
        // resize all containers
        Object.keys(this.containers)
            .forEach(type => this.resizeContainer(type, animate));

        this.$container.animate({
            width: this.width,
            height: this.height
        }, {
            queue: false,
            duration: animate ? 500 : 0
        });
    }

    /**
     * Enables/disables the filter indicating a video problem to the user.
     *
     * @param enable <tt>true</tt> to enable, <tt>false</tt> to disable
     */
    enableVideoProblemFilter (enable) {
        this.videoContainer.$video.toggleClass("videoProblemFilter", enable);
    }

    /**
     * Updates the src of the dominant speaker avatar
     */
    updateAvatar (avatarUrl) {
        $("#dominantSpeakerAvatar").attr('src', avatarUrl);
    }

    /**
     * Show or hide watermark.
     * @param {boolean} show
     */
    showWatermark (show) {
        $('.watermark').css('visibility', show ? 'visible' : 'hidden');
    }

    /**
     * Add container of specified type.
     * @param {string} type container type
     * @param {LargeContainer} container container to add.
     */
    addContainer (type, container) {
        if (this.containers[type]) {
            throw new Error(`container of type ${type} already exist`);
        }

        this.containers[type] = container;
        this.resizeContainer(type);
    }

    /**
     * Get Large container of specified type.
     * @param {string} type container type.
     * @returns {LargeContainer}
     */
    getContainer (type) {
        let container = this.containers[type];

        if (!container) {
            throw new Error(`container of type ${type} doesn't exist`);
        }

        return container;
    }

    /**
     * Remove Large container of specified type.
     * @param {string} type container type.
     */
    removeContainer (type) {
        if (!this.containers[type]) {
            throw new Error(`container of type ${type} doesn't exist`);
        }

        delete this.containers[type];
    }

    /**
     * Show Large container of specified type.
     * Does nothing if such container is already visible.
     * @param {string} type container type.
     * @returns {Promise}
     */
    showContainer (type) {
        if (this.state === type) {
            return Promise.resolve();
        }

        let oldContainer = this.containers[this.state];
        if (this.state === VideoContainerType) {
            this.showWatermark(false);
        }
        oldContainer.hide();

        this.state = type;
        let container = this.getContainer(type);

        return container.show().then(() => {
            if (type === VideoContainerType) {
                this.showWatermark(true);
            }
        });
    }
}
<commit_msg>LargeVideo: detach previous stream when attaching new one<commit_after>/* global $, APP, interfaceConfig */
/* jshint -W101 */

import UIUtil from "../util/UIUtil";
import UIEvents from "../../../service/UI/UIEvents";
import LargeContainer from './LargeContainer';
import BottomToolbar from '../toolbars/BottomToolbar';
import Avatar from "../avatar/Avatar";
import {createDeferred} from '../../util/helpers';

const avatarSize = interfaceConfig.DOMINANT_SPEAKER_AVATAR_SIZE;
const FADE_DURATION_MS = 300;

/**
 * Get stream id.
 * @param {JitsiTrack?} stream
 */
function getStreamOwnerId(stream) {
    if (!stream) {
        return;
    }
    if (stream.isLocal()) { // local stream doesn't have method "getParticipantId"
        return APP.conference.localId;
    } else {
        return stream.getParticipantId();
    }
}

/**
 * Returns an array of the video dimensions, so that it keeps it's aspect
 * ratio and fits available area with it's larger dimension. This method
 * ensures that whole video will be visible and can leave empty areas.
 *
 * @return an array with 2 elements, the video width and the video height
 */
function getDesktopVideoSize(videoWidth,
                             videoHeight,
                             videoSpaceWidth,
                             videoSpaceHeight) {

    let aspectRatio = videoWidth / videoHeight;

    let availableWidth = Math.max(videoWidth, videoSpaceWidth);
    let availableHeight = Math.max(videoHeight, videoSpaceHeight);

    videoSpaceHeight -= BottomToolbar.getFilmStripHeight();

    if (availableWidth / aspectRatio >= videoSpaceHeight) {
        availableHeight = videoSpaceHeight;
        availableWidth = availableHeight * aspectRatio;
    }

    if (availableHeight * aspectRatio >= videoSpaceWidth) {
        availableWidth = videoSpaceWidth;
        availableHeight = availableWidth / aspectRatio;
    }

    return [ availableWidth, availableHeight ];
}

/**
 * Returns an array of the video dimensions. It respects the
 * VIDEO_LAYOUT_FIT config, to fit the video to the screen, by hiding some parts
 * of it, or to fit it to the height or width.
 *
 * @param videoWidth the original video width
 * @param videoHeight the original video height
 * @param videoSpaceWidth the width of the video space
 * @param videoSpaceHeight the height of the video space
 * @return an array with 2 elements, the video width and the video height
 */
function getCameraVideoSize(videoWidth,
                            videoHeight,
                            videoSpaceWidth,
                            videoSpaceHeight) {

    let aspectRatio = videoWidth / videoHeight;

    let availableWidth = videoWidth;
    let availableHeight = videoHeight;

    if (interfaceConfig.VIDEO_LAYOUT_FIT == 'height') {
        availableHeight = videoSpaceHeight;
        availableWidth = availableHeight*aspectRatio;
    }
    else if (interfaceConfig.VIDEO_LAYOUT_FIT == 'width') {
        availableWidth = videoSpaceWidth;
        availableHeight = availableWidth/aspectRatio;
    }
    else if (interfaceConfig.VIDEO_LAYOUT_FIT == 'both') {
        availableWidth = Math.max(videoWidth, videoSpaceWidth);
        availableHeight = Math.max(videoHeight, videoSpaceHeight);

        if (availableWidth / aspectRatio < videoSpaceHeight) {
            availableHeight = videoSpaceHeight;
            availableWidth = availableHeight * aspectRatio;
        }

        if (availableHeight * aspectRatio < videoSpaceWidth) {
            availableWidth = videoSpaceWidth;
            availableHeight = availableWidth / aspectRatio;
        }
    }

    return [ availableWidth, availableHeight ];
}

/**
 * Returns an array of the video horizontal and vertical indents,
 * so that if fits its parent.
 *
 * @return an array with 2 elements, the horizontal indent and the vertical
 * indent
 */
function getCameraVideoPosition(videoWidth,
                                videoHeight,
                                videoSpaceWidth,
                                videoSpaceHeight) {
    // Parent height isn't completely calculated when we position the video in
    // full screen mode and this is why we use the screen height in this case.
    // Need to think it further at some point and implement it properly.
    if (UIUtil.isFullScreen()) {
        videoSpaceHeight = window.innerHeight;
    }

    let horizontalIndent = (videoSpaceWidth - videoWidth) / 2;
    let verticalIndent = (videoSpaceHeight - videoHeight) / 2;

    return { horizontalIndent, verticalIndent };
}

/**
 * Returns an array of the video horizontal and vertical indents.
 * Centers horizontally and top aligns vertically.
 *
 * @return an array with 2 elements, the horizontal indent and the vertical
 * indent
 */
function getDesktopVideoPosition(videoWidth,
                                 videoHeight,
                                 videoSpaceWidth,
                                 videoSpaceHeight) {

    let horizontalIndent = (videoSpaceWidth - videoWidth) / 2;

    let verticalIndent = 0;// Top aligned

    return { horizontalIndent, verticalIndent };
}

export const VideoContainerType = "video";

/**
 * Container for user video.
 */
class VideoContainer extends LargeContainer {
    // FIXME: With Temasys we have to re-select everytime
    get $video () {
        return $('#largeVideo');
    }

    get id () {
        return getStreamOwnerId(this.stream);
    }

    constructor (onPlay) {
        super();
        this.stream = null;
        this.videoType = null;

        this.isVisible = false;

        this.$avatar = $('#dominantSpeaker');
        this.$wrapper = $('#largeVideoWrapper');

        // This does not work with Temasys plugin - has to be a property to be
        // copied between new <object> elements
        //this.$video.on('play', onPlay);
        this.$video[0].onplay = onPlay;
    }

    /**
     * Get size of video element.
     * @returns {{width, height}}
     */
    getStreamSize () {
        let video = this.$video[0];
        return {
            width: video.videoWidth,
            height: video.videoHeight
        };
    }

    /**
     * Calculate optimal video size for specified container size.
     * @param {number} containerWidth container width
     * @param {number} containerHeight container height
     * @returns {{availableWidth, availableHeight}}
     */
    getVideoSize (containerWidth, containerHeight) {
        let { width, height } = this.getStreamSize();
        if (this.stream && this.isScreenSharing()) {
            return getDesktopVideoSize( width,
                                        height,
                                        containerWidth,
                                        containerHeight);
        } else {
            return getCameraVideoSize(  width,
                                        height,
                                        containerWidth,
                                        containerHeight);
        }
    }

    /**
     * Calculate optimal video position (offset for top left corner)
     * for specified video size and container size.
     * @param {number} width video width
     * @param {number} height video height
     * @param {number} containerWidth container width
     * @param {number} containerHeight container height
     * @returns {{horizontalIndent, verticalIndent}}
     */
    getVideoPosition (width, height, containerWidth, containerHeight) {
        if (this.stream && this.isScreenSharing()) {
            return getDesktopVideoPosition( width,
                                            height,
                                            containerWidth,
                                            containerHeight);
        } else {
            return getCameraVideoPosition(  width,
                                            height,
                                            containerWidth,
                                            containerHeight);
        }
    }

    resize (containerWidth, containerHeight, animate = false) {
        let [width, height]
            = this.getVideoSize(containerWidth, containerHeight);
        let { horizontalIndent, verticalIndent }
            = this.getVideoPosition(width, height,
                                    containerWidth, containerHeight);

        // update avatar position
        let top = containerHeight / 2 - avatarSize / 4 * 3;

        this.$avatar.css('top', top);

        this.$wrapper.animate({
            width: width,
            height: height,

            top: verticalIndent,
            bottom: verticalIndent,

            left: horizontalIndent,
            right: horizontalIndent
        }, {
            queue: false,
            duration: animate ? 500 : 0
        });
    }

    /**
     * Update video stream.
     * @param {JitsiTrack?} stream new stream
     * @param {string} videoType video type
     */
    setStream (stream, videoType) {
        // detach old stream
        if (this.stream) {
            this.stream.detach(this.$video[0]);
        }

        this.stream = stream;
        this.videoType = videoType;

        if (!stream) {
            return;
        }

        stream.attach(this.$video[0]);

        let flipX = stream.isLocal() && !this.isScreenSharing();
        this.$video.css({
            transform: flipX ? 'scaleX(-1)' : 'none'
        });
    }

    /**
     * Check if current video stream is screen sharing.
     * @returns {boolean}
     */
    isScreenSharing () {
        return this.videoType === 'desktop';
    }

    /**
     * Show or hide user avatar.
     * @param {boolean} show
     */
    showAvatar (show) {
        this.$avatar.css("visibility", show ? "visible" : "hidden");
    }

    // We are doing fadeOut/fadeIn animations on parent div which wraps
    // largeVideo, because when Temasys plugin is in use it replaces
    // <video> elements with plugin <object> tag. In Safari jQuery is
    // unable to store values on this plugin object which breaks all
    // animation effects performed on it directly.

    show () {
        // its already visible
        if (this.isVisible) {
            return Promise.resolve();
        }

        let $wrapper = this.$wrapper;
        return new Promise((resolve) => {
            this.$wrapper.css('visibility', 'visible').fadeTo(
                FADE_DURATION_MS,
                1,
                () => {
                    this.isVisible = true;
                    resolve();
                }
            );
        });
    }

    hide () {
        // its already hidden
        if (!this.isVisible) {
            return Promise.resolve();
        }

        return new Promise((resolve) => {
            this.$wrapper.fadeTo(FADE_DURATION_MS, 0, () => {
                this.$wrapper.css('visibility', 'hidden');
                this.isVisible = false;
                resolve();
            });
        });
    }
}

/**
 * Manager for all Large containers.
 */
export default class LargeVideoManager {
    constructor () {
        this.containers = {};

        this.state = VideoContainerType;
        this.videoContainer = new VideoContainer(() => this.resizeContainer(VideoContainerType));
        this.addContainer(VideoContainerType, this.videoContainer);

        this.width = 0;
        this.height = 0;

        this.$container = $('#largeVideoContainer');

        this.$container.css({
            display: 'inline-block'
        });

        if (interfaceConfig.SHOW_JITSI_WATERMARK) {
            let leftWatermarkDiv = this.$container.find("div.watermark.leftwatermark");

            leftWatermarkDiv.css({display: 'block'});

            leftWatermarkDiv.parent().attr('href', interfaceConfig.JITSI_WATERMARK_LINK);
        }

        if (interfaceConfig.SHOW_BRAND_WATERMARK) {
            let rightWatermarkDiv = this.$container.find("div.watermark.rightwatermark");

            rightWatermarkDiv.css({
                display: 'block',
                backgroundImage: 'url(images/rightwatermark.png)'
            });

            rightWatermarkDiv.parent().attr('href', interfaceConfig.BRAND_WATERMARK_LINK);
        }

        if (interfaceConfig.SHOW_POWERED_BY) {
            this.$container.children("a.poweredby").css({display: 'block'});
        }

        this.$container.hover(
            e => this.onHoverIn(e),
            e => this.onHoverOut(e)
        );
    }

    onHoverIn (e) {
        if (!this.state) {
            return;
        }
        let container = this.getContainer(this.state);
        container.onHoverIn(e);
    }

    onHoverOut (e) {
        if (!this.state) {
            return;
        }
        let container = this.getContainer(this.state);
        container.onHoverOut(e);
    }

    get id () {
        return this.videoContainer.id;
    }

    scheduleLargeVideoUpdate () {
        if (this.updateInProcess || !this.newStreamData) {
            return;
        }

        this.updateInProcess = true;

        let container = this.getContainer(this.state);

        container.hide().then(() => {
            let {id, stream, videoType, resolve} = this.newStreamData;
            this.newStreamData = null;

            console.info("hover in %s", id);
            this.state = VideoContainerType;
            this.videoContainer.setStream(stream, videoType);

            // change the avatar url on large
            this.updateAvatar(Avatar.getAvatarUrl(id));

            let isVideoMuted = stream ? stream.isMuted() : true;

            // show the avatar on large if needed
            this.videoContainer.showAvatar(isVideoMuted);

            let promise;

            // do not show stream if video is muted
            // but we still should show watermark
            if (isVideoMuted) {
                this.showWatermark(true);
                promise = Promise.resolve();
            } else {
                promise = this.videoContainer.show();
            }

            // resolve updateLargeVideo promise after everything is done
            promise.then(resolve);

            return promise;
        }).then(() => {
            // after everything is done check again if there are any pending new streams.
            this.updateInProcess = false;
            this.scheduleLargeVideoUpdate();
        });
    }

    /**
     * Update large video.
     * Switches to large video even if previously other container was visible.
     * @param userID the userID of the participant associated with the stream
     * @param {JitsiTrack?} stream new stream
     * @param {string?} videoType new video type
     * @returns {Promise}
     */
    updateLargeVideo (userID, stream, videoType) {
        if (this.newStreamData) {
            this.newStreamData.reject();
        }

        this.newStreamData = createDeferred();
        this.newStreamData.id = userID;
        this.newStreamData.stream = stream;
        this.newStreamData.videoType = videoType;

        this.scheduleLargeVideoUpdate();

        return this.newStreamData.promise;
    }

    /**
     * Update container size optionally taking side bar size into account.
     * @param {boolean} isSideBarVisible if side bar is visible.
     */
    updateContainerSize (isSideBarVisible) {
        this.width = UIUtil.getAvailableVideoWidth(isSideBarVisible);
        this.height = window.innerHeight;
    }

    /**
     * Resize Large container of specified type.
     * @param {string} type type of container which should be resized.
     * @param {boolean} [animate=false] if resize process should be animated.
     */
    resizeContainer (type, animate = false) {
        let container = this.getContainer(type);
        container.resize(this.width, this.height, animate);
    }

    /**
     * Resize all Large containers.
     * @param {boolean} animate if resize process should be animated.
     */
    resize (animate) {
        // resize all containers
        Object.keys(this.containers)
            .forEach(type => this.resizeContainer(type, animate));

        this.$container.animate({
            width: this.width,
            height: this.height
        }, {
            queue: false,
            duration: animate ? 500 : 0
        });
    }

    /**
     * Enables/disables the filter indicating a video problem to the user.
     *
     * @param enable <tt>true</tt> to enable, <tt>false</tt> to disable
     */
    enableVideoProblemFilter (enable) {
        this.videoContainer.$video.toggleClass("videoProblemFilter", enable);
    }

    /**
     * Updates the src of the dominant speaker avatar
     */
    updateAvatar (avatarUrl) {
        $("#dominantSpeakerAvatar").attr('src', avatarUrl);
    }

    /**
     * Show or hide watermark.
     * @param {boolean} show
     */
    showWatermark (show) {
        $('.watermark').css('visibility', show ? 'visible' : 'hidden');
    }

    /**
     * Add container of specified type.
     * @param {string} type container type
     * @param {LargeContainer} container container to add.
     */
    addContainer (type, container) {
        if (this.containers[type]) {
            throw new Error(`container of type ${type} already exist`);
        }

        this.containers[type] = container;
        this.resizeContainer(type);
    }

    /**
     * Get Large container of specified type.
     * @param {string} type container type.
     * @returns {LargeContainer}
     */
    getContainer (type) {
        let container = this.containers[type];

        if (!container) {
            throw new Error(`container of type ${type} doesn't exist`);
        }

        return container;
    }

    /**
     * Remove Large container of specified type.
     * @param {string} type container type.
     */
    removeContainer (type) {
        if (!this.containers[type]) {
            throw new Error(`container of type ${type} doesn't exist`);
        }

        delete this.containers[type];
    }

    /**
     * Show Large container of specified type.
     * Does nothing if such container is already visible.
     * @param {string} type container type.
     * @returns {Promise}
     */
    showContainer (type) {
        if (this.state === type) {
            return Promise.resolve();
        }

        let oldContainer = this.containers[this.state];
        if (this.state === VideoContainerType) {
            this.showWatermark(false);
        }
        oldContainer.hide();

        this.state = type;
        let container = this.getContainer(type);

        return container.show().then(() => {
            if (type === VideoContainerType) {
                this.showWatermark(true);
            }
        });
    }
}
<|endoftext|>


<commit_before>var assert = require('assert'),
		mongoose = require('mongoose');

var config = require('./../config');

var appSv	= require('./../service/app');

describe('app.update', function(){
	var appObj = {};
	var appInstance;

	// Connect to Mongo DB
	// Clean app database
	// Create a test app
	before(function(){
		console.log("START TEST APP.DELETE");
		mongoose.connect(config.mongodb.test);
	});

	// Disconnect
	after(function(){
		console.log("END TEST APP.DELETE");
		mongoose.disconnect();
	});

	beforeEach(function(done){
		appObj =
			{
				identifier: "de.unittest"
				, licenses: [{
			 		trialtype: "time"
			 		, value: 30
			 	}]
		 	};
		appSv.clean(function(err){
			if(err) throw err;
			appSv.create(appObj, function(err, app){
				if(err) throw err;
				appInstance = app;
				done();
			});
		});
	});

	it('should delete app and return deleted app', function(done){
		appSv.delete(appInstance, function(err, app){
			assert.ifError(err);
			assert.equal(app.identifier, appObj.identifier);
			done();
		});
	});
});<commit_msg>Add test cases for missing parameters<commit_after>var assert = require('assert'),
		mongoose = require('mongoose');

var config = require('./../config');

var appSv	= require('./../service/app');

describe('app.update', function(){
	var appObj = {};
	var appInstance;

	// Connect to Mongo DB
	// Clean app database
	// Create a test app
	before(function(){
		console.log("START TEST APP.DELETE");
		mongoose.connect(config.mongodb.test);
	});

	// Disconnect
	after(function(){
		console.log("END TEST APP.DELETE");
		mongoose.disconnect();
	});

	beforeEach(function(done){
		appObj =
			{
				identifier: "de.unittest"
				, licenses: [{
			 		trialtype: "time"
			 		, value: 30
			 	}]
		 	};
		appSv.clean(function(err){
			if(err) throw err;
			appSv.create(appObj, function(err, app){
				if(err) throw err;
				appInstance = app;
				done();
			});
		});
	});

	it('should return an error for undefined app parameter', function(done){
		var undefinedParameter;
		appSv.delete(undefinedParameter, function(err, app){
			assert.notEqual(err, null);
			done();
		});
	});

	it('should return an error for null app parameter', function(done){
		appSv.delete(null, function(err, app){
			assert.notEqual(err, null);
			done();
		});
	});

	it('should delete app and return deleted app', function(done){
		appSv.delete(appInstance, function(err, app){
			assert.ifError(err);
			assert.equal(app.identifier, appObj.identifier);
			done();
		});
	});
});<|endoftext|>