<commit_before>const moment = require('moment-timezone');
const Log = require('./debugPrint');
const settings = require('./settings');
const Cache = require('./cache');

const sScheduleDates = Symbol('Schedule Regular Dates');
const sScheduleDatesSpecial = Symbol('Schedule Special Dates');
const sCache = Symbol('Cache');
const sCachePopulated = Symbol('Cache Populated State');
const sCacheWaitCallbacks = Symbol('Cache Initiated Callbacks');
const sCacheTimer = Symbol('Cache Timer');

/**
 * @typedef ScheduleData
 * @type Object
 * @property {Moment} date Date this schedule data applies to
 * @property {Moment} openingTime Opening time for this date
 * @property {Moment} closingTime Closing time for this date
 * @property {String} type Whether this schedule data refers to an "Operating", "Closed" or "Refurbishment" status
 * @property {SpecialScheduleData[]} special Won't exist if no special times exist for this date
 */

/**
 * @typedef SpecialScheduleData
 * @type Object
 * @property {Moment} openingTime Opening time for this special schedule data
 * @property {Moment} closingTime Closing time for this special schedule data
 * @property {String} type Type of special schedule this is (eg. "Extra Magic Hours")
 */

function dateToDay(date) {
  // calculate the day since Unix Epoch
  //  .unix returns in UTC, so we convert to minutes and add on the utcOffset (then convert from minutes to days)
  //  finally we Math.floor to round downwards to get the current day as an integer
  return Math.floor(((date.unix() / 60) + date.utcOffset()) / 1440);
}

function parseDateTime(dateObject, varName) {
  let returnDate = dateObject;

  // check if it's already a valid Moment object
  if (!moment.isMoment(dateObject)) {
    // try and parse if this is a string
    returnDate = moment(dateObject, [
      moment.ISO_8601,
      settings.DefaultTimeFormat,
      settings.DefaultDateFormat,
      'YYYY-MM-DD',
    ]);

    // check if we ended up with a valid timestamp
    if (!returnDate.isValid()) {
      Log(`Invalid scheduleData.${varName}:`, returnDate);
      return false;
    }
  }

  // we got this far, success! return the new Moment object (or the original one if it was always good!)
  return returnDate;
}

/**
 * Schedule class to hold opening and closing times for parks and rides etc.
 * Supports standard and "special" opening times
 * @class
 */
class Schedule {
  /**
   * Create a new Schedule object
   * @param {Object} options
   * @param {String} options.id Unique Schedule ID for caching keys
   */
  constructor(options = {}) {
    // use Map for better structure (int -> data)
    //  int is the number of days since Unix Epoch
    this[sScheduleDates] = new Map();
    // also one for special hours (this is actually int -> data[] to support multiple special times)
    this[sScheduleDatesSpecial] = new Map();

    // setup our cache object
    if (options.id === undefined) throw new Error('No id passed for schedule object');
    this[sCache] = new Cache({
      prefix: `schedule_${options.id}`,
    });
    // store flag of whether we've populated ourselves with cache data yet
    this[sCachePopulated] = false;
    this[sCacheWaitCallbacks] = [];

    // try and populate cache
    this.CheckCacheStatus();
  }

  /**
   * Set schedule data for a date
   * @param {Object} scheduleData
   * @param {Moment|String} [scheduleData.date=scheduleData.openingTime] Moment.js date object (or a valid date String to be parsed by Moment JS). Will use openingTime if this is not supplied
   * @param {Moment|String} [scheduleData.openingTime] Moment.js date object of this day's opening time (or a valid date String to be parsed by Moment JS) (can be ignored if type is Closed)
   * @param {Moment|String} [scheduleData.closingTime] Moment.js date object of this day's closing time (or a valid date String to be parsed by Moment JS) (can be ignored if type is Closed)
   * @param {Boolean} [scheduleData.specialHours=false] Is this schedule data part of schedule special hours?
   * @param {String} [scheduleData.type=Operating] The schedule type. Normal schedules should always be "Operating", "Closed" or "Refurbishment". Special schedules can be any String (eg. Extra Magic Hours).
   * @returns {Boolean} success Returns true if the operation was a success and the data was actually changed
   */
  SetDate({
    // the day to set the schedule data for
    date = null,
    // opening time for this day
    openingTime = null,
    // closing time for this day
    closingTime = null,
    // is this special hours data? (default: false)
    specialHours = false,
    // the type of this schedule date (default: Operating)
    type = 'Operating',
  }) {
    let newDate = date.clone();
    let newOpeningTime = openingTime !== null ? openingTime.clone() : null;
    let newClosingTime = closingTime !== null ? closingTime.clone() : null;

    // if we haven't been supplied a date, use the opening time
    if (!newDate) newDate = openingTime.clone();

    // check our date is a valid momentjs object
    newDate = parseDateTime(newDate, 'date');

    // special case, if this is a closed date, support not passing in opening and closing times
    if (type === 'Closed') {
      if (!openingTime) newOpeningTime = newDate.startOf('day');
      if (!closingTime) newClosingTime = newDate.endOf('day');
    }

    newOpeningTime = parseDateTime(newOpeningTime, 'openingTime');
    newClosingTime = parseDateTime(newClosingTime, 'closingTime');

    // if any of our dates are invalid, return false
    if (!newDate || !newOpeningTime || !newClosingTime) return false;

    // calculate the days since Unix Epoch
    const day = dateToDay(newDate);

    // make sure opening and closing times are in the correct day!
    const todaySet = {
      year: newDate.year(),
      month: newDate.month(),
      date: newDate.date(),
    };
    newOpeningTime.set(todaySet);
    newClosingTime.set(todaySet);

    // work out if the closing time is in the following day
    if (newClosingTime.isBefore(newOpeningTime)) {
      // add 1 day if the closing time comes before the opening time (implying it's open past midnight!)
      newClosingTime.add(1, 'day');
    }

    // build schedule data object and add it to our schedule map
    if (!specialHours) {
      // check our schedule type is sane
      if (type !== 'Operating' && type !== 'Closed' && type !== 'Refurbishment') {
        Log(`Tried to use invalid schedule type ${type} for standard schedule data (must be Operating, Closed or Refurbishment)`);
        return false;
      }

      const newScheduleData = {
        date: newDate,
        openingTime: newOpeningTime,
        closingTime: newClosingTime,
        type,
      };

      // check if we already have this data for this day (don't invalidate cache etc if it hasn't changed)
      if (this[sScheduleDates].has(day)) {
        const checkDirtyObj = this[sScheduleDates].get(day);
        if (
          checkDirtyObj.date === newScheduleData.date
          && checkDirtyObj.openingTime === newScheduleData.openingTime
          && checkDirtyObj.closingTime === newScheduleData.closingTime
          && checkDirtyObj.type === newScheduleData.type) {
          // data is identical to existing object, don't update
          return false;
        }
      }

      // set this day's schedule data
      this[sScheduleDates].set(day, newScheduleData);

      // we have new data, so mark it as dirty to get cached
      this.RequestCache();
    } else {
      // special hours can't be Operating or Closed, that is for normal hours
      if (type === 'Operating' || type === 'Closed') {
        Log(`Tried to use invalid schedule type ${type} for special schedule data (can't be Operating or Closed)`);
        return false;
      }

      // add a new special hours array if we don't already have one
      if (!this[sScheduleDatesSpecial].has(day)) {
        this[sScheduleDatesSpecial].set(day, []);

        // we have new data, so mark it as dirty to get cached
        this.RequestCache();
      }

      const newSpecialScheduleData = {
        openingTime,
        closingTime,
        type,
      };

      // check we don't already have this special data in our array
      const newDataStringified = JSON.stringify(newSpecialScheduleData);
      const specialDayArray = this[sScheduleDatesSpecial].get(day);
      for (let i = 0; i < specialDayArray.length; i += 1) {
        if (JSON.stringify(specialDayArray[i]) === newDataStringified) {
          // this object already exists, so bail out
          return false;
        }
      }

      // add our new data to the specials array
      specialDayArray.push(newSpecialScheduleData);

      // we have new data, so mark it as dirty to get cached
      this.RequestCache();
    }

    return true;
  }

  /**
   * Set a range of dates with the same schedule data
   * @param {Object} scheduleData
   * @param {Moment|String} scheduleData.startDate Moment.js date object to start schedule date range (or a valid date String to be parsed by Moment JS)
   * @param {Moment|String} scheduleData.endDate Moment.js date object to end schedule date range (or a valid date String to be parsed by Moment JS)
   * @param {Moment|String} scheduleData.openingTime Moment.js date object of this day's opening time (or a valid date String to be parsed by Moment JS)
   * @param {Moment|String} scheduleData.closingTime Moment.js date object of this day's closing time (or a valid date String to be parsed by Moment JS)
   * @param {Boolean} [scheduleData.specialHours=false] Is this schedule data part of schedule special hours?
   * @param {String} [scheduleData.type=Operating] The schedule type. Normal schedules should always be "Operating", "Closed" or "Refurbishment". Special schedules can be any String (eg. Extra Magic Hours).
   * @returns {Boolean} success
   */
  SetRange({
    // first date of the range to set schedule for
    startDate = null,
    // first date of the range to set schedule for
    endDate = null,
    // opening time for this day
    openingTime = null,
    // closing time for this day
    closingTime = null,
    // is this special hours data? (default: false)
    specialHours = false,
    // the type of this schedule date (default: Operating)
    type = 'Operating',
  }) {
    // check our input dates are valid
    const newStartDate = parseDateTime(startDate, 'startDate');
    const newEndDate = parseDateTime(endDate, 'endDate');
    const newOpeningTime = parseDateTime(openingTime, 'openingTime');
    const newClosingTime = parseDateTime(closingTime, 'closingTime');

    // if any of our dates are invalid, return false
    if (!newStartDate || !newEndDate || !newOpeningTime || !newClosingTime) return false;

    // if any of our dates result in invalid data, return false
    let retValue = true;

    // add each day using SetDate
    for (let m = newStartDate; m.isSameOrBefore(newEndDate); m.add(1, 'days')) {
      // retValue AND= means this becomes false with any one failed result
      //  if we do fail, we also just keep going to try and get as much done as possible :)
      const successfulSet = this.SetDate({
        date: m,
        newOpeningTime,
        newClosingTime,
        specialHours,
        type,
      });

      retValue = retValue && successfulSet;
    }

    return retValue;
  }

  /**
   * Get schedule data for a specific date
   * @param {Object} dateData
   * @param {Moment|String} dateData.date Moment.js date object to fetch schedule data for (or a valid date String to be parsed by Moment JS)
   * @return {ScheduleData} scheduleResult Can be false if no data exists for the requested date
   */
  GetDate({
    date = null,
  }) {
    // check our date is valid
    const newDate = parseDateTime(date, 'date');
    if (!newDate) return undefined;

    // do we have this day in our schedule data?
    const day = dateToDay(newDate);

    if (!this[sScheduleDates].has(day)) return undefined;

    const dayData = this[sScheduleDates].get(day);
    // copy data into the return object (otherwise we end up modifying the actual date data!)
    const returnObject = {
      date: dayData.date,
      openingTime: dayData.openingTime,
      closingTime: dayData.closingTime,
      type: dayData.type,
    };

    // add special schedules if we have any!
    if (this[sScheduleDatesSpecial].has(day)) {
      returnObject.special = this[sScheduleDatesSpecial].get(day);
    }

    return returnObject;
  }

  /**
   * Get schedule data for a range of dates
   * @param {Object} dateData
   * @param {Moment|String} dateData.startDate Moment.js date object to fetch schedule data from (or a valid date String to be parsed by Moment JS)
   * @param {Moment|String} dateData.endDate Moment.js date object to fetch schedule data from (or a valid date String to be parsed by Moment JS)
   * @return {ScheduleData[]} scheduleResult Can be an empty array if there is no valid data (won't be null)
   */
  GetDateRange({
    startDate = null,
    endDate = null,
  }) {
    // check start and end date are valid
    const newStartDate = parseDateTime(startDate, 'startDate');
    const newEndDate = parseDateTime(endDate, 'endDate');
    if (!newStartDate || !newEndDate) return [];

    // fetch each day of the range and add it to our result
    const returnArray = [];
    for (let m = newStartDate; m.isSameOrBefore(newEndDate); m.add(1, 'days')) {
      const dateSchedule = this.GetDate({
        date: m,
      });
      if (dateSchedule !== undefined) {
        returnArray.push(dateSchedule);
      }
    }

    return returnArray;
  }

  /**
   * Get our cache object
   */
  get Cache() {
    return this[sCache];
  }

  /**
   * Request a cache to happen at the next best opportunity
   */
  RequestCache() {
    // if other caching was queued up, skip it
    if (this[sCacheTimer] !== undefined) {
      clearTimeout(this[sCacheTimer]);
    }

    // schedule caching in half a second
    this[sCacheTimer] = setTimeout(this.WriteToCache.bind(this), 500);
  }

  /**
   * Write schedule data to the cache
   */
  WriteToCache() {
    // if any timer exists for caching, clear it out, we're caching right now
    if (this[sCacheTimer] !== undefined) {
      clearTimeout(this[sCacheTimer]);
    }

    // delete old data (older than 3 days to be safe)
    const threeDaysAgo = dateToDay(moment().subtract(3, 'days'));
    const checkMapForDelete = (value, key, map) => {
      if (key <= threeDaysAgo) {
        map.delete(key);
      }
    };
    this[sScheduleDates].forEach(checkMapForDelete);
    this[sScheduleDatesSpecial].forEach(checkMapForDelete);

    // write data to cache
    return this.Cache.Set('dates', {
      // serialize Map as an array
      dates: [...this[sScheduleDates]],
      special: [...this[sScheduleDatesSpecial]],
    }, 60 * 60 * 6);
  }

  /**
   * Check the cache has been setup. Returns resolved Promise once it is
   */
  CheckCacheStatus() {
    if (this[sCachePopulated]) return Promise.resolve();

    return new Promise((resolve) => {
      // add our Promise resolve to our callback stack
      this[sCacheWaitCallbacks].push(resolve);

      // if we're the first to request the cache state, then actually do it...
      if (this[sCacheWaitCallbacks].length === 1) {
        // request cache read
        this.ReadFromCache().then(() => {
          // fire callbacks if they are present
          if (this[sCacheWaitCallbacks] && this[sCacheWaitCallbacks].length) {
            this[sCacheWaitCallbacks].forEach((cb) => {
              cb();
            });
            this[sCacheWaitCallbacks] = [];
          }
        });
      }
    });
  }

  /**
   * Read data from cache
   */
  ReadFromCache() {
    return this.Cache.Get('dates').then((data) => {
      // mark cache as populated so we don't try and read again
      this[sCachePopulated] = true;

      // if data is valid, update our internal state
      if (data && data.dates && data.special) {
        this[sScheduleDates] = Object.getPrototypeOf(data.dates) === Map.prototype ? data.dates : new Map(data.dates);
        this[sScheduleDatesSpecial] = Object.getPrototypeOf(data.special) === Map.prototype ? data.special : new Map(data.special);
      }

      return Promise.resolve(data);
    });
  }
}

module.exports = Schedule;
<commit_msg>[!] Fix not setting date ranges correct * Missing named argument passing to SetDate function<commit_after>const moment = require('moment-timezone');
const Log = require('./debugPrint');
const settings = require('./settings');
const Cache = require('./cache');

const sScheduleDates = Symbol('Schedule Regular Dates');
const sScheduleDatesSpecial = Symbol('Schedule Special Dates');
const sCache = Symbol('Cache');
const sCachePopulated = Symbol('Cache Populated State');
const sCacheWaitCallbacks = Symbol('Cache Initiated Callbacks');
const sCacheTimer = Symbol('Cache Timer');

/**
 * @typedef ScheduleData
 * @type Object
 * @property {Moment} date Date this schedule data applies to
 * @property {Moment} openingTime Opening time for this date
 * @property {Moment} closingTime Closing time for this date
 * @property {String} type Whether this schedule data refers to an "Operating", "Closed" or "Refurbishment" status
 * @property {SpecialScheduleData[]} special Won't exist if no special times exist for this date
 */

/**
 * @typedef SpecialScheduleData
 * @type Object
 * @property {Moment} openingTime Opening time for this special schedule data
 * @property {Moment} closingTime Closing time for this special schedule data
 * @property {String} type Type of special schedule this is (eg. "Extra Magic Hours")
 */

function dateToDay(date) {
  // calculate the day since Unix Epoch
  //  .unix returns in UTC, so we convert to minutes and add on the utcOffset (then convert from minutes to days)
  //  finally we Math.floor to round downwards to get the current day as an integer
  return Math.floor(((date.unix() / 60) + date.utcOffset()) / 1440);
}

function parseDateTime(dateObject, varName) {
  let returnDate = dateObject;

  // check if it's already a valid Moment object
  if (!moment.isMoment(dateObject)) {
    // try and parse if this is a string
    returnDate = moment(dateObject, [
      moment.ISO_8601,
      settings.DefaultTimeFormat,
      settings.DefaultDateFormat,
      'YYYY-MM-DD',
    ]);

    // check if we ended up with a valid timestamp
    if (!returnDate.isValid()) {
      Log(`Invalid scheduleData.${varName}:`, returnDate);
      return false;
    }
  }

  // we got this far, success! return the new Moment object (or the original one if it was always good!)
  return returnDate;
}

/**
 * Schedule class to hold opening and closing times for parks and rides etc.
 * Supports standard and "special" opening times
 * @class
 */
class Schedule {
  /**
   * Create a new Schedule object
   * @param {Object} options
   * @param {String} options.id Unique Schedule ID for caching keys
   */
  constructor(options = {}) {
    // use Map for better structure (int -> data)
    //  int is the number of days since Unix Epoch
    this[sScheduleDates] = new Map();
    // also one for special hours (this is actually int -> data[] to support multiple special times)
    this[sScheduleDatesSpecial] = new Map();

    // setup our cache object
    if (options.id === undefined) throw new Error('No id passed for schedule object');
    this[sCache] = new Cache({
      prefix: `schedule_${options.id}`,
    });
    // store flag of whether we've populated ourselves with cache data yet
    this[sCachePopulated] = false;
    this[sCacheWaitCallbacks] = [];

    // try and populate cache
    this.CheckCacheStatus();
  }

  /**
   * Set schedule data for a date
   * @param {Object} scheduleData
   * @param {Moment|String} [scheduleData.date=scheduleData.openingTime] Moment.js date object (or a valid date String to be parsed by Moment JS). Will use openingTime if this is not supplied
   * @param {Moment|String} [scheduleData.openingTime] Moment.js date object of this day's opening time (or a valid date String to be parsed by Moment JS) (can be ignored if type is Closed)
   * @param {Moment|String} [scheduleData.closingTime] Moment.js date object of this day's closing time (or a valid date String to be parsed by Moment JS) (can be ignored if type is Closed)
   * @param {Boolean} [scheduleData.specialHours=false] Is this schedule data part of schedule special hours?
   * @param {String} [scheduleData.type=Operating] The schedule type. Normal schedules should always be "Operating", "Closed" or "Refurbishment". Special schedules can be any String (eg. Extra Magic Hours).
   * @returns {Boolean} success Returns true if the operation was a success and the data was actually changed
   */
  SetDate({
    // the day to set the schedule data for
    date = null,
    // opening time for this day
    openingTime = null,
    // closing time for this day
    closingTime = null,
    // is this special hours data? (default: false)
    specialHours = false,
    // the type of this schedule date (default: Operating)
    type = 'Operating',
  }) {
    let newDate = date.clone();
    let newOpeningTime = openingTime !== null ? openingTime.clone() : null;
    let newClosingTime = closingTime !== null ? closingTime.clone() : null;

    // if we haven't been supplied a date, use the opening time
    if (!newDate) newDate = openingTime.clone();

    // check our date is a valid momentjs object
    newDate = parseDateTime(newDate, 'date');

    // special case, if this is a closed date, support not passing in opening and closing times
    if (type === 'Closed') {
      if (!openingTime) newOpeningTime = newDate.startOf('day');
      if (!closingTime) newClosingTime = newDate.endOf('day');
    }

    newOpeningTime = parseDateTime(newOpeningTime, 'openingTime');
    newClosingTime = parseDateTime(newClosingTime, 'closingTime');

    // if any of our dates are invalid, return false
    if (!newDate || !newOpeningTime || !newClosingTime) return false;

    // calculate the days since Unix Epoch
    const day = dateToDay(newDate);

    // make sure opening and closing times are in the correct day!
    const todaySet = {
      year: newDate.year(),
      month: newDate.month(),
      date: newDate.date(),
    };
    newOpeningTime.set(todaySet);
    newClosingTime.set(todaySet);

    // work out if the closing time is in the following day
    if (newClosingTime.isBefore(newOpeningTime)) {
      // add 1 day if the closing time comes before the opening time (implying it's open past midnight!)
      newClosingTime.add(1, 'day');
    }

    // build schedule data object and add it to our schedule map
    if (!specialHours) {
      // check our schedule type is sane
      if (type !== 'Operating' && type !== 'Closed' && type !== 'Refurbishment') {
        Log(`Tried to use invalid schedule type ${type} for standard schedule data (must be Operating, Closed or Refurbishment)`);
        return false;
      }

      const newScheduleData = {
        date: newDate,
        openingTime: newOpeningTime,
        closingTime: newClosingTime,
        type,
      };

      // check if we already have this data for this day (don't invalidate cache etc if it hasn't changed)
      if (this[sScheduleDates].has(day)) {
        const checkDirtyObj = this[sScheduleDates].get(day);
        if (
          checkDirtyObj.date === newScheduleData.date
          && checkDirtyObj.openingTime === newScheduleData.openingTime
          && checkDirtyObj.closingTime === newScheduleData.closingTime
          && checkDirtyObj.type === newScheduleData.type) {
          // data is identical to existing object, don't update
          return false;
        }
      }

      // set this day's schedule data
      this[sScheduleDates].set(day, newScheduleData);

      // we have new data, so mark it as dirty to get cached
      this.RequestCache();
    } else {
      // special hours can't be Operating or Closed, that is for normal hours
      if (type === 'Operating' || type === 'Closed') {
        Log(`Tried to use invalid schedule type ${type} for special schedule data (can't be Operating or Closed)`);
        return false;
      }

      // add a new special hours array if we don't already have one
      if (!this[sScheduleDatesSpecial].has(day)) {
        this[sScheduleDatesSpecial].set(day, []);

        // we have new data, so mark it as dirty to get cached
        this.RequestCache();
      }

      const newSpecialScheduleData = {
        openingTime,
        closingTime,
        type,
      };

      // check we don't already have this special data in our array
      const newDataStringified = JSON.stringify(newSpecialScheduleData);
      const specialDayArray = this[sScheduleDatesSpecial].get(day);
      for (let i = 0; i < specialDayArray.length; i += 1) {
        if (JSON.stringify(specialDayArray[i]) === newDataStringified) {
          // this object already exists, so bail out
          return false;
        }
      }

      // add our new data to the specials array
      specialDayArray.push(newSpecialScheduleData);

      // we have new data, so mark it as dirty to get cached
      this.RequestCache();
    }

    return true;
  }

  /**
   * Set a range of dates with the same schedule data
   * @param {Object} scheduleData
   * @param {Moment|String} scheduleData.startDate Moment.js date object to start schedule date range (or a valid date String to be parsed by Moment JS)
   * @param {Moment|String} scheduleData.endDate Moment.js date object to end schedule date range (or a valid date String to be parsed by Moment JS)
   * @param {Moment|String} scheduleData.openingTime Moment.js date object of this day's opening time (or a valid date String to be parsed by Moment JS)
   * @param {Moment|String} scheduleData.closingTime Moment.js date object of this day's closing time (or a valid date String to be parsed by Moment JS)
   * @param {Boolean} [scheduleData.specialHours=false] Is this schedule data part of schedule special hours?
   * @param {String} [scheduleData.type=Operating] The schedule type. Normal schedules should always be "Operating", "Closed" or "Refurbishment". Special schedules can be any String (eg. Extra Magic Hours).
   * @returns {Boolean} success
   */
  SetRange({
    // first date of the range to set schedule for
    startDate = null,
    // first date of the range to set schedule for
    endDate = null,
    // opening time for this day
    openingTime = null,
    // closing time for this day
    closingTime = null,
    // is this special hours data? (default: false)
    specialHours = false,
    // the type of this schedule date (default: Operating)
    type = 'Operating',
  }) {
    // check our input dates are valid
    const newStartDate = parseDateTime(startDate, 'startDate');
    const newEndDate = parseDateTime(endDate, 'endDate');
    const newOpeningTime = parseDateTime(openingTime, 'openingTime');
    const newClosingTime = parseDateTime(closingTime, 'closingTime');

    // if any of our dates are invalid, return false
    if (!newStartDate || !newEndDate || !newOpeningTime || !newClosingTime) return false;

    // if any of our dates result in invalid data, return false
    let retValue = true;

    // add each day using SetDate
    for (let m = newStartDate; m.isSameOrBefore(newEndDate); m.add(1, 'days')) {
      // retValue AND= means this becomes false with any one failed result
      //  if we do fail, we also just keep going to try and get as much done as possible :)
      const successfulSet = this.SetDate({
        date: m,
        openingTime: newOpeningTime,
        closingTime: newClosingTime,
        specialHours,
        type,
      });

      retValue = retValue && successfulSet;
    }

    return retValue;
  }

  /**
   * Get schedule data for a specific date
   * @param {Object} dateData
   * @param {Moment|String} dateData.date Moment.js date object to fetch schedule data for (or a valid date String to be parsed by Moment JS)
   * @return {ScheduleData} scheduleResult Can be false if no data exists for the requested date
   */
  GetDate({
    date = null,
  }) {
    // check our date is valid
    const newDate = parseDateTime(date, 'date');
    if (!newDate) return undefined;

    // do we have this day in our schedule data?
    const day = dateToDay(newDate);

    if (!this[sScheduleDates].has(day)) return undefined;

    const dayData = this[sScheduleDates].get(day);
    // copy data into the return object (otherwise we end up modifying the actual date data!)
    const returnObject = {
      date: dayData.date,
      openingTime: dayData.openingTime,
      closingTime: dayData.closingTime,
      type: dayData.type,
    };

    // add special schedules if we have any!
    if (this[sScheduleDatesSpecial].has(day)) {
      returnObject.special = this[sScheduleDatesSpecial].get(day);
    }

    return returnObject;
  }

  /**
   * Get schedule data for a range of dates
   * @param {Object} dateData
   * @param {Moment|String} dateData.startDate Moment.js date object to fetch schedule data from (or a valid date String to be parsed by Moment JS)
   * @param {Moment|String} dateData.endDate Moment.js date object to fetch schedule data from (or a valid date String to be parsed by Moment JS)
   * @return {ScheduleData[]} scheduleResult Can be an empty array if there is no valid data (won't be null)
   */
  GetDateRange({
    startDate = null,
    endDate = null,
  }) {
    // check start and end date are valid
    const newStartDate = parseDateTime(startDate, 'startDate');
    const newEndDate = parseDateTime(endDate, 'endDate');
    if (!newStartDate || !newEndDate) return [];

    // fetch each day of the range and add it to our result
    const returnArray = [];
    for (let m = newStartDate; m.isSameOrBefore(newEndDate); m.add(1, 'days')) {
      const dateSchedule = this.GetDate({
        date: m,
      });
      if (dateSchedule !== undefined) {
        returnArray.push(dateSchedule);
      }
    }

    return returnArray;
  }

  /**
   * Get our cache object
   */
  get Cache() {
    return this[sCache];
  }

  /**
   * Request a cache to happen at the next best opportunity
   */
  RequestCache() {
    // if other caching was queued up, skip it
    if (this[sCacheTimer] !== undefined) {
      clearTimeout(this[sCacheTimer]);
    }

    // schedule caching in half a second
    this[sCacheTimer] = setTimeout(this.WriteToCache.bind(this), 500);
  }

  /**
   * Write schedule data to the cache
   */
  WriteToCache() {
    // if any timer exists for caching, clear it out, we're caching right now
    if (this[sCacheTimer] !== undefined) {
      clearTimeout(this[sCacheTimer]);
    }

    // delete old data (older than 3 days to be safe)
    const threeDaysAgo = dateToDay(moment().subtract(3, 'days'));
    const checkMapForDelete = (value, key, map) => {
      if (key <= threeDaysAgo) {
        map.delete(key);
      }
    };
    this[sScheduleDates].forEach(checkMapForDelete);
    this[sScheduleDatesSpecial].forEach(checkMapForDelete);

    // write data to cache
    return this.Cache.Set('dates', {
      // serialize Map as an array
      dates: [...this[sScheduleDates]],
      special: [...this[sScheduleDatesSpecial]],
    }, 60 * 60 * 6);
  }

  /**
   * Check the cache has been setup. Returns resolved Promise once it is
   */
  CheckCacheStatus() {
    if (this[sCachePopulated]) return Promise.resolve();

    return new Promise((resolve) => {
      // add our Promise resolve to our callback stack
      this[sCacheWaitCallbacks].push(resolve);

      // if we're the first to request the cache state, then actually do it...
      if (this[sCacheWaitCallbacks].length === 1) {
        // request cache read
        this.ReadFromCache().then(() => {
          // fire callbacks if they are present
          if (this[sCacheWaitCallbacks] && this[sCacheWaitCallbacks].length) {
            this[sCacheWaitCallbacks].forEach((cb) => {
              cb();
            });
            this[sCacheWaitCallbacks] = [];
          }
        });
      }
    });
  }

  /**
   * Read data from cache
   */
  ReadFromCache() {
    return this.Cache.Get('dates').then((data) => {
      // mark cache as populated so we don't try and read again
      this[sCachePopulated] = true;

      // if data is valid, update our internal state
      if (data && data.dates && data.special) {
        this[sScheduleDates] = Object.getPrototypeOf(data.dates) === Map.prototype ? data.dates : new Map(data.dates);
        this[sScheduleDatesSpecial] = Object.getPrototypeOf(data.special) === Map.prototype ? data.special : new Map(data.special);
      }

      return Promise.resolve(data);
    });
  }
}

module.exports = Schedule;
<|endoftext|>


<commit_before>/**
 * Adds the capability to load SeaJS modules from NodeJS applications.
 * @author lifesinger@gmail.com
 */

var MP = module.constructor.prototype;
var _compile = MP._compile;
var moduleStack = [];
var helper = require('./helper');

MP._compile = function(content, filename) {
  moduleStack.push(this);
  try {
    return _compile.call(this, content, filename);
  }
  finally {
    moduleStack.pop();
  }
};

/**
 * Only support this format:
 *   define(function(require, exports, module) { ... })
 */
global.define = function(factory) {
  var returned = factory;
  var module = moduleStack[moduleStack.length - 1] || require.main;

  if (typeof factory === 'function') {
    returned = factory.call(
        global,
        helper.createRequire(module),
        module.exports,
        module);
  }

  if (returned !== undefined) {
    module.exports = returned;
  }
};

/**
 * Thanks to
 *  - https://github.com/ajaxorg/node-amd-loader/blob/master/lib/amd-loader.js
 */
<commit_msg>add some notes<commit_after>/**
 * Adds the capability to load SeaJS modules from NodeJS applications.
 * @author lifesinger@gmail.com
 */

var MP = module.constructor.prototype;
var _compile = MP._compile;
var moduleStack = [];
var helper = require('./helper');

MP._compile = function(content, filename) {
  moduleStack.push(this);
  try {
    return _compile.call(this, content, filename);
  }
  finally {
    moduleStack.pop();
  }
};

/**
 * Only support this format:
 *   define(function(require, exports, module) { ... })
 */
global.define = function(factory) {
  var returned = factory;
  var module = moduleStack[moduleStack.length - 1] || require.main;

  if (typeof factory === 'function') {
    returned = factory.call(
        global,
        helper.createRequire(module),
        module.exports,
        module);
  }

  if (returned !== undefined) {
    module.exports = returned;
  }
};

/**
 * Thanks to
 *  - https://github.com/joyent/node/blob/master/lib/module.js
 *  - https://github.com/ajaxorg/node-amd-loader/blob/master/lib/amd-loader.js
 */
<|endoftext|>


<commit_before>const is = require('@sindresorhus/is');
const handlebars = require('handlebars');
const releaseNotesHbs = require('./changelog/hbs-template');

const versioning = require('../../versioning');

const { appName, appSlug, urls } = require('../../config/app-strings');

handlebars.registerHelper('encodeURIComponent', encodeURIComponent);

module.exports = {
  getPrBody,
};

function getTableDefinition(config) {
  const res = [];
  for (const header of config.prBodyColumns) {
    const value = config.prBodyDefinitions[header];
    res.push({ header, value });
  }
  return res;
}

function getNonEmptyColumns(definitions, rows) {
  const res = [];
  for (const column of definitions) {
    const { header } = column;
    for (const row of rows) {
      if (row[header] && row[header].length) {
        if (!res.includes(header)) {
          res.push(header);
        }
      }
    }
  }
  return res;
}

async function getPrBody(config) {
  config.upgrades.forEach(upgrade => {
    /* eslint-disable no-param-reassign */
    const { homepage, sourceUrl, sourceDirectory, changelogUrl } = upgrade;
    const references = [];
    if (homepage) {
      references.push(`[homepage](${homepage})`);
    }
    if (sourceUrl) {
      let fullUrl = sourceUrl;
      if (sourceDirectory) {
        fullUrl =
          sourceUrl.replace(/\/?$/, '/') + sourceDirectory.replace('^/?/', '');
      }
      references.push(`[source](${fullUrl})`);
    }
    if (changelogUrl) {
      references.push(`[changelog](${changelogUrl})`);
    }
    upgrade.references = references.join(', ');
    const {
      fromVersion,
      toVersion,
      newValue,
      newDigestShort,
      updateType,
      versionScheme,
    } = upgrade;
    // istanbul ignore if
    if (updateType === 'minor') {
      try {
        const { getMinor } = versioning.get(versionScheme);
        if (getMinor(fromVersion) === getMinor(toVersion)) {
          upgrade.updateType = 'patch';
        }
      } catch (err) {
        // do nothing
      }
    }
    if (newDigestShort) {
      if (updateType === 'pin') {
        upgrade.newValue = config.newDigestShort;
      }
      if (newValue) {
        upgrade.newValue = newValue + '@' + newDigestShort;
      } else {
        upgrade.newValue = newDigestShort;
      }
    } else if (updateType !== 'lockFileMaintenance') {
      upgrade.newValue = newValue;
    }
    /* eslint-enable no-param-reassign */
  });
  const tableDefinitions = getTableDefinition(config);
  const tableValues = config.upgrades.map(upgrade => {
    const res = {};
    for (const column of tableDefinitions) {
      const { header, value } = column;
      try {
        res[header] = handlebars
          .compile(value)(upgrade)
          .replace(/^``$/, '');
      } catch (err) /* istanbul ignore next */ {
        logger.warn({ header, value, err }, 'Handlebars compilation error');
      }
    }
    return res;
  });
  const tableColumns = getNonEmptyColumns(tableDefinitions, tableValues);
  let prBody = '';
  // istanbul ignore if
  if (config.prBanner && !config.isGroup) {
    prBody += handlebars.compile(config.prBanner)(config) + '\n\n';
  }
  prBody += '\n\nThis PR contains the following updates:\n\n';
  prBody += '| ' + tableColumns.join(' | ') + ' |\n';
  prBody += '|' + tableColumns.map(() => '---|').join('') + '\n';
  const rows = [];
  for (const row of tableValues) {
    let val = '|';
    for (const column of tableColumns) {
      val += ` ${row[column].replace(/^@/, '@')} |`;
    }
    val += '\n';
    rows.push(val);
  }
  const uniqueRows = [...new Set(rows)];
  prBody += uniqueRows.join('');
  prBody += '\n\n';

  const notes = [];
  for (const upgrade of config.upgrades) {
    if (is.nonEmptyArray(upgrade.prBodyNotes)) {
      for (const note of upgrade.prBodyNotes) {
        try {
          const res = handlebars
            .compile(note)(upgrade)
            .trim();
          if (res && res.length) {
            notes.push(res);
          }
        } catch (err) {
          logger.warn({ note }, 'Error compiling upgrade note');
        }
      }
    }
  }
  const uniqueNotes = [...new Set(notes)];
  prBody += uniqueNotes.join('\n\n');
  prBody += '\n\n';

  if (config.upgrades.some(upgrade => upgrade.gitRef)) {
    prBody +=
      ':abcd: If you wish to disable git hash updates, add `":disableDigestUpdates"` to the extends array in your config.\n\n';
  }

  if (config.updateType === 'lockFileMaintenance') {
    prBody +=
      ':wrench: This Pull Request updates `package.json` lock files to use the latest dependency versions.\n\n';
  }

  if (config.isPin) {
    prBody += `:pushpin: **Important**: ${appName} will wait until you have merged this Pin PR before creating any *upgrade* PRs for the affected packages. Add the preset \`:preserveSemverRanges\` your config if you instead don't wish to pin dependencies.\n\n`;
  }
  if (config.hasReleaseNotes) {
    let releaseNotes =
      '\n\n---\n\n' + handlebars.compile(releaseNotesHbs)(config) + '\n\n';
    releaseNotes = releaseNotes.replace(/### \[`vv/g, '### [`v');
    // Generic replacements/link-breakers

    // Put a zero width space after every # followed by a digit
    releaseNotes = releaseNotes.replace(/#(\d)/gi, '#$1');
    // Put a zero width space after every @ symbol to prevent unintended hyperlinking
    releaseNotes = releaseNotes.replace(/@/g, '@');
    releaseNotes = releaseNotes.replace(/(`\[?@)/g, '$1');
    releaseNotes = releaseNotes.replace(/([a-z]@)/gi, '$1');
    releaseNotes = releaseNotes.replace(/\/compare\/@/g, '/compare/@');
    releaseNotes = releaseNotes.replace(
      /(\(https:\/\/[^)]*?)\.\.\.@/g,
      '$1...@'
    );
    releaseNotes = releaseNotes.replace(
      /([\s(])#(\d+)([)\s]?)/g,
      '$1#$2$3'
    );
    // convert escaped backticks back to `
    const backTickRe = /`([^/]*?)`/g;
    releaseNotes = releaseNotes.replace(backTickRe, '`$1`');
    releaseNotes = releaseNotes.replace(/`#(\d+)`/g, '`#$1`');
    prBody += releaseNotes;
  }
  prBody += `\n\n---\n\n### ${appName} configuration\n\n`;
  prBody += `:date: **Schedule**: `;
  if (
    config.schedule &&
    config.schedule !== 'at any time' &&
    config.schedule[0] !== 'at any time'
  ) {
    prBody += `"${config.schedule}"`;
    if (config.timezone) {
      prBody += ` in timezone ${config.timezone}.`;
    } else {
      prBody += ` (UTC).`;
    }
  } else {
    prBody += 'At any time (no schedule defined).';
  }

  prBody += '\n\n';
  prBody += ':vertical_traffic_light: **Automerge**: ';
  if (config.automerge) {
    const branchStatus = await platform.getBranchStatus(
      config.branchName,
      config.requiredStatusChecks
    );
    // istanbul ignore if
    if (branchStatus === 'failed') {
      prBody += 'Disabled due to failing status checks.';
    } else {
      prBody += 'Enabled.';
    }
  } else {
    prBody +=
      'Disabled by config. Please merge this manually once you are satisfied.';
  }
  prBody += '\n\n';
  prBody += ':recycle: **Rebasing**: ';
  if (config.rebaseStalePrs) {
    prBody += 'Whenever PR is stale';
  } else {
    prBody += 'Whenever PR becomes conflicted';
  }
  if (config.platform === 'github') {
    prBody += `, or if you modify the PR title to begin with "\`rebase!\`".\n\n`;
  } else {
    prBody += '.\n\n';
  }
  if (config.recreateClosed) {
    prBody += `:ghost: **Immortal**: This PR will be recreated if closed unmerged. Get [config help](${
      urls.help
    }) if that's undesired.\n\n`;
  } else {
    prBody += `:no_bell: **Ignore**: Close this PR and you won't be reminded about ${
      config.upgrades.length === 1 ? 'this update' : 'these updates'
    } again.\n\n`;
  }
  prBody += `\n\n---\n\n - [ ] <!-- ${appSlug}-rebase -->If you want to rebase/retry this PR, check this box\n\n`;
  // istanbul ignore if
  if (config.global) {
    if (config.global.prBanner) {
      prBody = config.global.prBanner + '\n\n' + prBody;
    }
    if (config.global.prFooter) {
      prBody = prBody + '\n---\n\n' + config.global.prFooter;
    }
  }
  prBody = prBody.trim();
  prBody = prBody.replace(/\n\n\n+/g, '\n\n');

  prBody = platform.getPrBody(prBody);
  return prBody;
}
<commit_msg>fix: tree/master sourceDirectory<commit_after>const is = require('@sindresorhus/is');
const handlebars = require('handlebars');
const releaseNotesHbs = require('./changelog/hbs-template');

const versioning = require('../../versioning');

const { appName, appSlug, urls } = require('../../config/app-strings');

handlebars.registerHelper('encodeURIComponent', encodeURIComponent);

module.exports = {
  getPrBody,
};

function getTableDefinition(config) {
  const res = [];
  for (const header of config.prBodyColumns) {
    const value = config.prBodyDefinitions[header];
    res.push({ header, value });
  }
  return res;
}

function getNonEmptyColumns(definitions, rows) {
  const res = [];
  for (const column of definitions) {
    const { header } = column;
    for (const row of rows) {
      if (row[header] && row[header].length) {
        if (!res.includes(header)) {
          res.push(header);
        }
      }
    }
  }
  return res;
}

async function getPrBody(config) {
  config.upgrades.forEach(upgrade => {
    /* eslint-disable no-param-reassign */
    const { homepage, sourceUrl, sourceDirectory, changelogUrl } = upgrade;
    const references = [];
    if (homepage) {
      references.push(`[homepage](${homepage})`);
    }
    if (sourceUrl) {
      let fullUrl = sourceUrl;
      if (sourceDirectory) {
        fullUrl =
          sourceUrl.replace(/\/?$/, '/') +
          'tree/master/' +
          sourceDirectory.replace('^/?/', '');
      }
      references.push(`[source](${fullUrl})`);
    }
    if (changelogUrl) {
      references.push(`[changelog](${changelogUrl})`);
    }
    upgrade.references = references.join(', ');
    const {
      fromVersion,
      toVersion,
      newValue,
      newDigestShort,
      updateType,
      versionScheme,
    } = upgrade;
    // istanbul ignore if
    if (updateType === 'minor') {
      try {
        const { getMinor } = versioning.get(versionScheme);
        if (getMinor(fromVersion) === getMinor(toVersion)) {
          upgrade.updateType = 'patch';
        }
      } catch (err) {
        // do nothing
      }
    }
    if (newDigestShort) {
      if (updateType === 'pin') {
        upgrade.newValue = config.newDigestShort;
      }
      if (newValue) {
        upgrade.newValue = newValue + '@' + newDigestShort;
      } else {
        upgrade.newValue = newDigestShort;
      }
    } else if (updateType !== 'lockFileMaintenance') {
      upgrade.newValue = newValue;
    }
    /* eslint-enable no-param-reassign */
  });
  const tableDefinitions = getTableDefinition(config);
  const tableValues = config.upgrades.map(upgrade => {
    const res = {};
    for (const column of tableDefinitions) {
      const { header, value } = column;
      try {
        res[header] = handlebars
          .compile(value)(upgrade)
          .replace(/^``$/, '');
      } catch (err) /* istanbul ignore next */ {
        logger.warn({ header, value, err }, 'Handlebars compilation error');
      }
    }
    return res;
  });
  const tableColumns = getNonEmptyColumns(tableDefinitions, tableValues);
  let prBody = '';
  // istanbul ignore if
  if (config.prBanner && !config.isGroup) {
    prBody += handlebars.compile(config.prBanner)(config) + '\n\n';
  }
  prBody += '\n\nThis PR contains the following updates:\n\n';
  prBody += '| ' + tableColumns.join(' | ') + ' |\n';
  prBody += '|' + tableColumns.map(() => '---|').join('') + '\n';
  const rows = [];
  for (const row of tableValues) {
    let val = '|';
    for (const column of tableColumns) {
      val += ` ${row[column].replace(/^@/, '@')} |`;
    }
    val += '\n';
    rows.push(val);
  }
  const uniqueRows = [...new Set(rows)];
  prBody += uniqueRows.join('');
  prBody += '\n\n';

  const notes = [];
  for (const upgrade of config.upgrades) {
    if (is.nonEmptyArray(upgrade.prBodyNotes)) {
      for (const note of upgrade.prBodyNotes) {
        try {
          const res = handlebars
            .compile(note)(upgrade)
            .trim();
          if (res && res.length) {
            notes.push(res);
          }
        } catch (err) {
          logger.warn({ note }, 'Error compiling upgrade note');
        }
      }
    }
  }
  const uniqueNotes = [...new Set(notes)];
  prBody += uniqueNotes.join('\n\n');
  prBody += '\n\n';

  if (config.upgrades.some(upgrade => upgrade.gitRef)) {
    prBody +=
      ':abcd: If you wish to disable git hash updates, add `":disableDigestUpdates"` to the extends array in your config.\n\n';
  }

  if (config.updateType === 'lockFileMaintenance') {
    prBody +=
      ':wrench: This Pull Request updates `package.json` lock files to use the latest dependency versions.\n\n';
  }

  if (config.isPin) {
    prBody += `:pushpin: **Important**: ${appName} will wait until you have merged this Pin PR before creating any *upgrade* PRs for the affected packages. Add the preset \`:preserveSemverRanges\` your config if you instead don't wish to pin dependencies.\n\n`;
  }
  if (config.hasReleaseNotes) {
    let releaseNotes =
      '\n\n---\n\n' + handlebars.compile(releaseNotesHbs)(config) + '\n\n';
    releaseNotes = releaseNotes.replace(/### \[`vv/g, '### [`v');
    // Generic replacements/link-breakers

    // Put a zero width space after every # followed by a digit
    releaseNotes = releaseNotes.replace(/#(\d)/gi, '#$1');
    // Put a zero width space after every @ symbol to prevent unintended hyperlinking
    releaseNotes = releaseNotes.replace(/@/g, '@');
    releaseNotes = releaseNotes.replace(/(`\[?@)/g, '$1');
    releaseNotes = releaseNotes.replace(/([a-z]@)/gi, '$1');
    releaseNotes = releaseNotes.replace(/\/compare\/@/g, '/compare/@');
    releaseNotes = releaseNotes.replace(
      /(\(https:\/\/[^)]*?)\.\.\.@/g,
      '$1...@'
    );
    releaseNotes = releaseNotes.replace(
      /([\s(])#(\d+)([)\s]?)/g,
      '$1#$2$3'
    );
    // convert escaped backticks back to `
    const backTickRe = /`([^/]*?)`/g;
    releaseNotes = releaseNotes.replace(backTickRe, '`$1`');
    releaseNotes = releaseNotes.replace(/`#(\d+)`/g, '`#$1`');
    prBody += releaseNotes;
  }
  prBody += `\n\n---\n\n### ${appName} configuration\n\n`;
  prBody += `:date: **Schedule**: `;
  if (
    config.schedule &&
    config.schedule !== 'at any time' &&
    config.schedule[0] !== 'at any time'
  ) {
    prBody += `"${config.schedule}"`;
    if (config.timezone) {
      prBody += ` in timezone ${config.timezone}.`;
    } else {
      prBody += ` (UTC).`;
    }
  } else {
    prBody += 'At any time (no schedule defined).';
  }

  prBody += '\n\n';
  prBody += ':vertical_traffic_light: **Automerge**: ';
  if (config.automerge) {
    const branchStatus = await platform.getBranchStatus(
      config.branchName,
      config.requiredStatusChecks
    );
    // istanbul ignore if
    if (branchStatus === 'failed') {
      prBody += 'Disabled due to failing status checks.';
    } else {
      prBody += 'Enabled.';
    }
  } else {
    prBody +=
      'Disabled by config. Please merge this manually once you are satisfied.';
  }
  prBody += '\n\n';
  prBody += ':recycle: **Rebasing**: ';
  if (config.rebaseStalePrs) {
    prBody += 'Whenever PR is stale';
  } else {
    prBody += 'Whenever PR becomes conflicted';
  }
  if (config.platform === 'github') {
    prBody += `, or if you modify the PR title to begin with "\`rebase!\`".\n\n`;
  } else {
    prBody += '.\n\n';
  }
  if (config.recreateClosed) {
    prBody += `:ghost: **Immortal**: This PR will be recreated if closed unmerged. Get [config help](${
      urls.help
    }) if that's undesired.\n\n`;
  } else {
    prBody += `:no_bell: **Ignore**: Close this PR and you won't be reminded about ${
      config.upgrades.length === 1 ? 'this update' : 'these updates'
    } again.\n\n`;
  }
  prBody += `\n\n---\n\n - [ ] <!-- ${appSlug}-rebase -->If you want to rebase/retry this PR, check this box\n\n`;
  // istanbul ignore if
  if (config.global) {
    if (config.global.prBanner) {
      prBody = config.global.prBanner + '\n\n' + prBody;
    }
    if (config.global.prFooter) {
      prBody = prBody + '\n---\n\n' + config.global.prFooter;
    }
  }
  prBody = prBody.trim();
  prBody = prBody.replace(/\n\n\n+/g, '\n\n');

  prBody = platform.getPrBody(prBody);
  return prBody;
}
<|endoftext|>


<commit_before>"use strict";

var package_json    = require('./../package.json');
var nodemailer      = require('nodemailer');
var _               = require('lodash');
var request         = require('request');
var Email           = require('./email');
var SmtpapiHeaders  = require('./smtpapi_headers');
var Sendgrid = function(api_user, api_key, options) {

  if( !(this instanceof Sendgrid) ) {
    return new Sendgrid(api_user, api_key, options);
  }

  var _this         = this;
  this.options      = options || {};
  this.options.web  = this.options.web || {};
  this.options.port = this.options.port || 587;

  var send = function() {
    if ( _this.options.api === 'smtp') {
      smtp.apply(this, arguments);
      return true;
    }

    web.apply( this, arguments );
  };

  /*
   * Sends an email via web and returns true if the
   * message was sent successfully.
   *
   * @param  {Email|Object}  email     An email object or a hash that has
   *                                   the values for the email to be sent.
   * @param  {Function}      callback  A function to call when the processing is done.
   *                                   This parameter is optional.
   */
  var web = function(email, cb) {
    var callback    = cb || function() { };
    if (email.constructor !== Email) {
      email = new Email(email);
    }

    _sendWeb.bind(this)(email, callback);
  }.bind(this);

  /*
   * Sends an email via SMTP and returns true if the
   * message was sent successfully.
   *
   * @param    {Email|Object}  email                     An email object or a hash that has
   *                                                     the values for the email to be sent.
   * @param    {Object}        nodeMailerOptions         Extra options for nodeMailer. i.e. Message-Id
   *                                                     This parameter is optional.
   * @param    {Function}      callback                  A function to call when the processing is done.
   *                                                     This parameter is optional.
   */
  var smtp = function(email, nodeMailerOptions, cb) {

    // Support a callback without nodeMailerOptions
    if (! cb && typeof nodeMailerOptions === "function") {
      callback = nodeMailerOptions;
      nodeMailerOptions = null;
    }

    var callback = cb || function() { };

    if (email.constructor !== Email) {
      email = new Email(email);
    }

    _sendSmtp(email, nodeMailerOptions, callback);
  };

  /*
   * Psuedo-private methods
   */
  var _sendWeb = function(email, callback) {
    var postOptions = {
      method    : 'POST',
      uri       : "https://sendgrid.com/api/mail.send.json"
    };

    var options = _.merge(this.options.web, postOptions);

    var req   = request(options, function(err, resp, body) {
      var json;

      if(err) return callback(err, null);

      try {
        json = JSON.parse(body);
      } catch (e) {
        return callback(new Error(e), null);
      }

      if (json.message !== 'success') {
        var error = 'sendgrid error';
        if (json.errors) { error = json.errors.shift(); }
        return callback(new Error(error), null);
      }

      return callback(null, json);
    });

    var form          = email.toWebFormat();
    form['api_user']  = api_user;
    form['api_key']   = api_key;

    var reqForm = req.form();

    var _reqFormAppend = function(field, value) {
      reqForm.append(field, value);
    };

    for (var field in form) {
      var value = form[field];
      if (value && value.filename) {
        if (value.cid) {
          reqForm.append("content["+value.filename+"]", value.cid);
        }
        reqForm.append("files["+value.filename+"]", value.content, {filename: value.filename, contentType: value.contentType});
      } else {
        try {
          if(!Array.isArray(value)){
            //reqForm.append(field, value);
            _reqFormAppend.bind(this)(field, value);
          } else {
            value.forEach(_reqFormAppend.bind(this, field));
          }
        } catch(err) {}
      }
    }
  };

  var _sendSmtp = function(email, nodeMailerOptions, callback) {
    // SMTP settings
    var smtp_settings = {
      host: "smtp.sendgrid.net",
      port: parseInt(_this.port, 10),
      requiresAuth: true,
      auth: {
        user: api_user,
        pass: api_key
      }
    };

    if (smtp_settings.port == 465) {
      smtp_settings['secureConnection'] = true;
    }

    var smtpTransport = nodemailer.createTransport(_this.SMTP, smtp_settings);

    var smtpParams = email.toSmtpFormat();

    if (_.isObject(nodeMailerOptions)) {
      _.extend(smtpParams, nodeMailerOptions);
    }

    smtpTransport.sendMail(smtpParams, function(error, response) {

      smtpTransport.close();
      if(error) { return callback(new Error(error.message), null);}

      return callback(null, {'message': 'success'});
    });
  };

  /*
   * Expose public API calls
   */
  this.version         = package_json.version;
  this.SMTP            = "SMTP";
  this.Email           = Email;
  this.SmtpapiHeaders  = SmtpapiHeaders;
  this.api_user        = api_user;
  this.api_key         = api_key;
  this.web             = web;
  this.smtp            = smtp;
  this.send            = send;
  this.options         = this.options;
  this.port            = this.options.port;
  return this;
};

module.exports = Sendgrid;
<commit_msg>Fix callback<commit_after>"use strict";

var package_json    = require('./../package.json');
var nodemailer      = require('nodemailer');
var _               = require('lodash');
var request         = require('request');
var Email           = require('./email');
var SmtpapiHeaders  = require('./smtpapi_headers');
var Sendgrid = function(api_user, api_key, options) {

  if( !(this instanceof Sendgrid) ) {
    return new Sendgrid(api_user, api_key, options);
  }

  var _this         = this;
  this.options      = options || {};
  this.options.web  = this.options.web || {};
  this.options.port = this.options.port || 587;

  var send = function() {
    if ( _this.options.api === 'smtp') {
      smtp.apply(this, arguments);
      return true;
    }

    web.apply( this, arguments );
  };

  /*
   * Sends an email via web and returns true if the
   * message was sent successfully.
   *
   * @param  {Email|Object}  email     An email object or a hash that has
   *                                   the values for the email to be sent.
   * @param  {Function}      callback  A function to call when the processing is done.
   *                                   This parameter is optional.
   */
  var web = function(email, callback) {
    var callback    = callback || function() { };
    if (email.constructor !== Email) {
      email = new Email(email);
    }

    _sendWeb.bind(this)(email, callback);
  }.bind(this);

  /*
   * Sends an email via SMTP and returns true if the
   * message was sent successfully.
   *
   * @param    {Email|Object}  email                     An email object or a hash that has
   *                                                     the values for the email to be sent.
   * @param    {Object}        nodeMailerOptions         Extra options for nodeMailer. i.e. Message-Id
   *                                                     This parameter is optional.
   * @param    {Function}      callback                  A function to call when the processing is done.
   *                                                     This parameter is optional.
   */
  var smtp = function(email, nodeMailerOptions, callback) {

    // Support a callback without nodeMailerOptions
    if (! callback && typeof nodeMailerOptions === "function") {
      callback = nodeMailerOptions;
      nodeMailerOptions = null;
    }

    var callback = callback || function() { };

    if (email.constructor !== Email) {
      email = new Email(email);
    }

    _sendSmtp(email, nodeMailerOptions, callback);
  };

  /*
   * Psuedo-private methods
   */
  var _sendWeb = function(email, callback) {
    var postOptions = {
      method    : 'POST',
      uri       : "https://sendgrid.com/api/mail.send.json"
    };

    var options = _.merge(this.options.web, postOptions);

    var req   = request(options, function(err, resp, body) {
      var json;

      if(err) return callback(err, null);

      try {
        json = JSON.parse(body);
      } catch (e) {
        return callback(new Error(e), null);
      }

      if (json.message !== 'success') {
        var error = 'sendgrid error';
        if (json.errors) { error = json.errors.shift(); }
        return callback(new Error(error), null);
      }

      return callback(null, json);
    });

    var form          = email.toWebFormat();
    form['api_user']  = api_user;
    form['api_key']   = api_key;

    var reqForm = req.form();

    var _reqFormAppend = function(field, value) {
      reqForm.append(field, value);
    };

    for (var field in form) {
      var value = form[field];
      if (value && value.filename) {
        if (value.cid) {
          reqForm.append("content["+value.filename+"]", value.cid);
        }
        reqForm.append("files["+value.filename+"]", value.content, {filename: value.filename, contentType: value.contentType});
      } else {
        try {
          if(!Array.isArray(value)){
            //reqForm.append(field, value);
            _reqFormAppend.bind(this)(field, value);
          } else {
            value.forEach(_reqFormAppend.bind(this, field));
          }
        } catch(err) {}
      }
    }
  };

  var _sendSmtp = function(email, nodeMailerOptions, callback) {
    // SMTP settings
    var smtp_settings = {
      host: "smtp.sendgrid.net",
      port: parseInt(_this.port, 10),
      requiresAuth: true,
      auth: {
        user: api_user,
        pass: api_key
      }
    };

    if (smtp_settings.port == 465) {
      smtp_settings['secureConnection'] = true;
    }

    var smtpTransport = nodemailer.createTransport(_this.SMTP, smtp_settings);

    var smtpParams = email.toSmtpFormat();

    if (_.isObject(nodeMailerOptions)) {
      _.extend(smtpParams, nodeMailerOptions);
    }

    smtpTransport.sendMail(smtpParams, function(error, response) {

      smtpTransport.close();
      if(error) { return callback(new Error(error.message), null);}

      return callback(null, {'message': 'success'});
    });
  };

  /*
   * Expose public API calls
   */
  this.version         = package_json.version;
  this.SMTP            = "SMTP";
  this.Email           = Email;
  this.SmtpapiHeaders  = SmtpapiHeaders;
  this.api_user        = api_user;
  this.api_key         = api_key;
  this.web             = web;
  this.smtp            = smtp;
  this.send            = send;
  this.options         = this.options;
  this.port            = this.options.port;
  return this;
};

module.exports = Sendgrid;
<|endoftext|>


<commit_before>exports.sendmail = function (Client, opts) {
	opts = opts || {};
	if (typeof opts.to == "string") {
		opts.to = [ opts.to ];
	} else if (!opts.to || !opts.to.length) {
		throw { "code": 1, "message": "You must define destination e-mails using 'to' parameter" };
	}
	if (!opts.content) {
		throw { "code": 2, "message": "You must define e-mail data using 'content' parameter" };
	}

	var client = new Client(),
	    to_index = 0;

	client.connect(opts.host || "127.0.0.1", opts.port || 25);
	client.on("connect", function () {
		if (opts.auth) {
			client.authenticate(
				opts.auth[0],
				opts.auth.length > 1 ? opts.auth[1] : "",
				opts.auth.length > 2 ? opts.auth[2] : "cram-md5"
			);
			return;
		} else {
			client.mailFrom(opts.from || "root@localhost");
		}
	});
	client.on("auth", function (success) {
		if (success) {
			client.mailFrom(opts.from || "root@localhost");
		} else if (opts.failure) {
			opts.failure(err);
		}
	});
	client.on("mailFrom", function () {
		client.rcptTo(opts.to[to_index++]);
	});
	client.on("rcptTo", function () {
		if (opts.to.length > to_index) {
			client.rcptTo(opts.to[to_index++]);
			return;
		}

		client.data(opts.content);
	});
	client.on("data", function () {
		client.close();
	});
	client.on("close", function () {
		if (opts.success) {
			opts.success();
		}
		return;
	});
	client.on("err", function (err) {
		if (opts.failure) {
			opts.failure(err);
		}
		return;
	});
};
<commit_msg>added "contentPath" to sendmail to read email content from a file<commit_after>var path = require("path"),
    fs = require("fs");

exports.sendmail = function (Client, opts) {
	opts = opts || {};
	if (typeof opts.to == "string") {
		opts.to = [ opts.to ];
	} else if (!opts.to || !opts.to.length) {
		throw { "code": 1, "message": "You must define destination e-mails using 'to' parameter" };
	}
	if (!opts.content && !opts.contentPath) {
		throw { "code": 2, "message": "You must define e-mail data/file using 'content' or 'contentPath' parameter" };
	}

	var client = new Client(),
	    to_index = 0;

	client.connect(opts.host || "127.0.0.1", opts.port || 25);
	client.on("connect", function () {
		if (opts.auth) {
			client.authenticate(
				opts.auth[0],
				opts.auth.length > 1 ? opts.auth[1] : "",
				opts.auth.length > 2 ? opts.auth[2] : "cram-md5"
			);
			return;
		} else {
			client.mailFrom(opts.from || "root@localhost");
		}
	});
	client.on("auth", function (success) {
		if (success) {
			client.mailFrom(opts.from || "root@localhost");
		} else if (opts.failure) {
			opts.failure(err);
		}
	});
	client.on("mailFrom", function () {
		client.rcptTo(opts.to[to_index++]);
	});
	client.on("rcptTo", function () {
		if (opts.to.length > to_index) {
			client.rcptTo(opts.to[to_index++]);
			return;
		}

		if (opts.contentPath) {
			path.exists(opts.contentPath, function (exists) {
				if (!exists) {
					return opts.failure({ "code": 3, "message": "Content file " + opts.contentPath + " does not exist" });
				}
				fs.readFile(opts.contentPath, function (err, data) {
					if (err) {
						return opts.failure(err);
					}
					client.data(data);
				});
			});
		} else {
			client.data(opts.content);
		}
	});
	client.on("data", function () {
		client.close();
	});
	client.on("close", function () {
		if (opts.success) {
			opts.success();
		}
		return;
	});
	client.on("err", function (err) {
		if (opts.failure) {
			opts.failure(err);
		}
		return;
	});
};
<|endoftext|>


<commit_before>/**
 * Rich Text Editor
 */
'use strict';

(function (document, app, CKEDITOR) {
    const cfg = {
        contentsCss: '',
        customConfig: '',
        disableNativeSpellChecker: true,
        extraAllowedContent: 'article section(*)',
        fillEmptyBlocks: false,
        format_tags: 'p;h2;h3',
        height: '30rem',
        language: 'de',
        mediabrowserUrl: '/file/browser',
        removeDialogTabs: 'link:advanced;link:target',
        stylesSet: false,
        toolbar: [
            {
                name: 'all',
                items: ['Undo', 'Redo', 'Bold', 'Italic', 'Link', 'Unlink', 'Format', 'BulletedList', 'NumberedList', 'Blockquote', 'Media', 'Table', 'Detail']
            }
        ]
    };

    document.addEventListener('DOMContentLoaded', function () {
        Array.prototype.forEach.call(document.querySelectorAll('textarea[data-type=rte]'), function (item) {
            CKEDITOR.replace(item, cfg);
        });
    });
})(document, app, CKEDITOR);
<commit_msg>Move rte stuff to rte js<commit_after>/**
 * Rich Text Editor
 */
'use strict';

(function (document, app, CKEDITOR) {
    document.addEventListener('DOMContentLoaded', function () {
        Array.prototype.forEach.call(document.querySelectorAll('textarea[data-type=rte]'), function (item) {
            CKEDITOR.replace(item, {
                contentsCss: '',
                customConfig: '',
                disableNativeSpellChecker: true,
                extraAllowedContent: 'article section(*)',
                fillEmptyBlocks: false,
                format_tags: 'p;h2;h3',
                height: '30rem',
                language: 'de',
                mediabrowserUrl: '/file/browser',
                removeDialogTabs: 'link:advanced;link:target',
                stylesSet: false,
                toolbar: [
                    {
                        name: 'all',
                        items: ['Undo', 'Redo', 'Bold', 'Italic', 'Link', 'Unlink', 'Format', 'BulletedList', 'NumberedList', 'Blockquote', 'Media', 'Table', 'Detail']
                    }
                ]
            });
        });
    });
})(document, app, CKEDITOR);
<|endoftext|>


<commit_before>/**
 * Module dependencies.
 */
var decisions = require('decisions')
  , pkginfo = require('pkginfo')
  , path = require('path')
  , fs = require('fs')
  , existsSync = fs.existsSync || path.existsSync; // <=0.6

/**
 * Component-ized settings.
 *
 * To utilize this component within an application, configure the IoC loader
 * to use common components.
 *
 *     IoC.loader(require('bixby-common'));
 */
exports = module.exports = function() {
  var root = path.dirname(pkginfo.find(require.main));
  var env = process.env.NODE_ENV || 'development';

  var settings = decisions.createSettings();

  var dirname = path.resolve(root, 'etc')
    , exts = [ '.toml', '.yaml', '.json' ]
    , ext, i, len, file;

  // load default settings
  for (i = 0, len = exts.length; i < len; ++i) {
    ext = exts[i];
    file = path.join(dirname, 'defaults' + ext);
    if (existsSync(file)) {
      settings.load(file);
      break;
    }
  }

  // load environment-specific settings
  for (i = 0, len = exts.length; i < len; ++i) {
    ext = exts[i];
    file = path.join(dirname, env + ext);
    if (existsSync(file)) {
      settings.load(file);
      break;
    }
  }

  return settings;
}

/**
 * Component annotations.
 */
exports['@singleton'] = true;
exports['@require'] = [];
<commit_msg>Load user-specific settings.<commit_after>/**
 * Module dependencies.
 */
var decisions = require('decisions')
  , pkginfo = require('pkginfo')
  , path = require('path-os')
  , fs = require('fs')
  , existsSync = fs.existsSync || path.existsSync; // <=0.6

/**
 * Component-ized settings.
 *
 * To utilize this component within an application, configure the IoC loader
 * to use common components.
 *
 *     IoC.loader(require('bixby-common'));
 */
exports = module.exports = function() {
  var root = path.dirname(pkginfo.find(require.main));
  var env = process.env.NODE_ENV || 'development';

  var settings = decisions.createSettings();

  var dirname = path.resolve(root, 'etc')
    , exts = [ '.toml', '.yaml', '.json' ]
    , ext, i, len, file;

  // load default settings
  for (i = 0, len = exts.length; i < len; ++i) {
    ext = exts[i];
    file = path.join(dirname, 'defaults' + ext);
    if (existsSync(file)) {
      settings.load(file);
      break;
    }
  }

  // load environment-specific settings
  for (i = 0, len = exts.length; i < len; ++i) {
    ext = exts[i];
    file = path.join(dirname, env + ext);
    if (existsSync(file)) {
      settings.load(file);
      break;
    }
  }

  // load user-specific settings
  var pkg = pkginfo.read(require.main).package;
  var vend = pkg.author && pkg.author.organization
           ? path.join(pkg.author.organization, pkg.name)
           : pkg.name;

  for (i = 0, len = exts.length; i < len; ++i) {
    ext = exts[i];
    file = path.configdir(vend + ext);

    if (existsSync(file)) {
      settings.load(file);
      break;
    }
  }

  return settings;
}

/**
 * Component annotations.
 */
exports['@singleton'] = true;
exports['@require'] = [];
<|endoftext|>


<commit_before>var readline = require("readline");

function Settings (database) {
    this.database = database;
}

Settings.prototype.get = function (key, getter, callback) {
    var database = this.database;

    database.get("select value from `settings` where key = ? ;", [key], function (err, row) {
        if (err)
            throw err;

        if (row)
            callback(row.value);
        else {
            var prompt = readline.createInterface({
                input: process.stdin,
                output: process.stdout
            });

            getter(prompt, function (value) {
                database.run("insert into `settings` values (?, ?) ;", [key, value], function (err) {
                    if (err)
                        throw err;
                })

                callback(value);
                prompt.close();
            })
        }
    })
}

module.exports = Settings;<commit_msg>Implemented batch get of settings<commit_after>var readline = require("readline");

function Settings (database) {
    this.database = database;
}

Settings.prototype.get = function (key, getter, callback) {
    var database = this.database;

    database.get("select value from `settings` where key = ? ;", [key], function (err, row) {
        if (err)
            throw err;

        if (row)
            callback(row.value);
        else {
            var prompt = readline.createInterface({
                input: process.stdin,
                output: process.stdout
            });

            getter(prompt, function (value) {
                database.run("insert into `settings` values (?, ?) ;", [key, value], function (err) {
                    if (err)
                        throw err;
                })

                callback(value);
                prompt.close();
            })
        }
    })
}

Settings.prototype.batchGet = function (keys, getters, callback) {
    if (keys.length < 1 || getters.length < 1) {
        callback([]);
        return
    }

    var that = this;

    that.get(keys.shift(), getters.shift(), function (value) {
        that.batchGet(keys, getters, function (rest) {
            rest.unshift(value);
            callback(rest);
        })
    })
}

module.exports = Settings;<|endoftext|>


<commit_before>"use strict";

var request = require('request')
  , crypto = require('crypto')
  , querystring = require('querystring')
  , url = require('url');

var PROTO = 1; //Version of Beebotte Protocol

/** @constructor */
function Connection (bbt) {
  this.bbt = bbt;
  this.connection = null;
  this.connected;
}

Connection.prototype.onConnection = function() {
  for(var c in this.bbt.subscriptions) {
    this.do_subscribe(this.bbt.subscriptions[c]);
  }
}

Connection.prototype.getWsUrl = function() {
  var p = (this.bbt.transport.ssl === true)? this.bbt.transport.sport : this.bbt.transport.port;
  return ((this.bbt.transport.ssl === true)? 'https://' : 'http://' ) + this.bbt.transport.ws_host + ':' + p;
}

Connection.prototype.connect = function () {
  if( this.connection && this.connection.connected ) return this;

  var self = this;
  if( self.bbt.transport.token ) {
    var query =  'token=' + self.bbt.transport.token + '&username=' + (self.bbt.userinfo.username || '');
  } else {
    var query =  'key=' + self.bbt.transport.key + '&username=' + (self.bbt.userinfo.username || '');
  }
  this.connection = require('socket.io-client')(self.getWsUrl(), {query: query });
  this.connection.on('connect', function () {
    if(self.connected === false) {
      self.connected = true;
      self.onConnection();
      self.bbt.emit('reconnected');
    }else {
      self.connected = true;
      self.bbt.emit('connected');
    }
  });

  this.connection.on('error', function (err) {
    self.bbt.emit('error', err);
  });

  this.connection.on('disconnect', function () {
    self.connected = false;
    self.bbt.emit('disconnected');
  });

  this.connection.on('message', function (msg) {
    if(msg.channel && msg.resource) {
      var subscription = self.bbt.getAnySubscription(msg.channel, msg.resource);
      if(subscription) {
        subscription.fct(msg);
      }else {
        //console.log('Warning! non subscribed message: ' + JSON.stringify(msg));
      }
    } else {
      //console.log('Warning! non conform message: ' + JSON.stringify(msg));
    }
  });

  return self.bbt;
}

Connection.prototype.disconnect = function () {
  if(this.connection.connected) {
    this.connection.disconnect();
    this.connection.removeAllListeners();
  };
  return this.bbt;
}

//for internal use only
Connection.prototype.send_auth = function(sig, source) {
  var self = this;
  if(self.send('control', 'authenticate', {auth: sig.auth, source: source})) {
    this.authenticated = true;
    return true;
  }else {
    this.authenticated = false
    return false;
  }
}

Connection.prototype.subscribe = function(args, callback) {
  var subscription = this.bbt.getSubscription(args.channel, args.resource);

  if(subscription) {
    if(subscription.update(args)) this.do_subscribe(subscription);
  }else {
    subscription = this.bbt.addSubscription(args, callback);
    this.do_subscribe(subscription);
  }
  return this.bbt;
}

Connection.prototype.do_subscribe = function(subscription) {
  var self = this;
  if(!self.connection.connected) return self.bbt;
  var args = {};
  args.channel = subscription.channel;
  args.resource = subscription.resource || self.bbt.wildcard;
  args.ttl = args.ttl || 0;
  args.read = subscription.read;
  args.write = subscription.write;
  if(typeof self.bbt.userinfo !== 'undefined') {
    args.userinfo = self.bbt.userinfo;
  }
  //Authentication required for write access and for read access to private or presence resources
  function authNeeded() {
    if(args.write === true) return true;
    if(args.channel.indexOf('private-') === 0) return true;
    if(args.channel.indexOf('presence-') === 0) return true;
    return false;
  }

  /* Utility function for signing connection
   * Will be used with function based authentication method
   * (self sufficient authentication - requires Secret Key)
   */
  function signSubscription ( ) {
    var to_sign = args.sid + ':' + args.channel + '.' + args.resource + ':ttl=' + args.ttl + ':read=' + args.read + ':write=' + args.write;

    if( self.bbt.userinfo.userid ) to_sign = to_sign + ':userid=' + self.bbt.userinfo.userid;

    var signature = crypto.createHmac('sha1', self.bbt.transport.secretKey).update(to_sign).digest('base64');
    var auth = { auth: self.bbt.transport.key + ':' + signature };

    if( self.bbt.userinfo.userid ) auth.userid = self.bbt.userinfo.userid;

    return auth;
  }

  if( authNeeded() ) {
    if( ! self.bbt.transport.auth_endpoint && self.bbt.transport.auth_method !== 'fct' ) {
      self.bbt.emit('subscribeError', new Error('Authentication error: Missing authentication endpoint!'), args);
      return self.bbt;
    }
    if(self.connection && self.connection.connected && self.connection.io.engine.id) {
      args.sid = self.connection.io.engine.id;
      if(self.bbt.transport.auth_method === 'get') {

        var uri = self.bbt.transport.auth_endpoint;
        var options = {
            url: url.parse(uri),
            qs: args,
            method: 'GET',
        }

        request(options, function (error, response, body) {
            if(error) {
              self.bbt.emit('subscribeError', error, args);
              return self.bbt;
            }
            if (!error && response.statusCode == 200) {
              if( typeof body === 'string' ) {
                try {
                  body = JSON.parse(body);
                } catch(e) {
                  self.bbt.emit('subscribeError', new Error('Unable to authenticate client'), args);
                  return self.bbt;
                }
              }
              args.sig = body.auth;
              if( body.userid ) args.userid = body.userid;
              if(self.send('control', 'subscribe', args)) {
                subscription.subscribe();
                self.bbt.emit('subscribed', subscription);
                return self.bbt;
              }else {
                  self.bbt.emit('subscribeError', new Error('Unexpected error encountered while subscribing'), args);
                  return self.bbt;
              }
            }else {
                self.bbt.emit('subscribeError', new Error('Unable to authenticate client'), args);
                return self.bbt;
            }
        });
      }else if (self.bbt.transport.auth_method === 'post') {
        uri = self.bbt.transport.auth_endpoint;
        options = {
          url: url.parse(uri),
          method: 'POST',
          body: JSON.stringify(args)
        }

        options.headers = {
          'Content-Type': 'application/json'
        };

        request(options, function (error, response, body) {
            if(error) {
              self.bbt.emit('subscribeError', new Error('Unable to authenticate client'), args);
              return self.bbt;
            }
            if (!error && response.statusCode == 200) {
              if( typeof body === 'string' ) {
                try {
                  body = JSON.parse(body);
                } catch(e) {
                  self.bbt.emit('subscribeError', new Error('Unable to authenticate client'), args);
                  return self.bbt;
                }
              }
              args.sig = body.auth;
              if( body.userid ) args.userid = body.userid;
              if(self.send('control', 'subscribe', args)) {
                subscription.subscribe();
                self.bbt.emit('subscribed', subscription);
                return self.bbt;
              }else {
                  self.bbt.emit('subscribeError', new Error('Unexpected error encountered while subscribing'), args);
                  return self.bbt;
              }
            }else {
              self.bbt.emit('subscribeError', new Error('Unable to authenticate client'), args);
              return self.bbt;
            }
        });
      }else if (self.bbt.transport.auth_method === 'fct') {
        if( self.bbt.transport.token ) {
          args.token = self.bbt.transport.token;
          if( self.bbt.userinfo.userid ) args.userid = self.bbt.userinfo.userid;
        } else {
          var sig = signSubscription( );
          if( !sig ) {
            self.bbt.emit('subscribeError', new Error('Unable to authenticate client'), args);
            return self.bbt;
          }
          args.sig = sig.auth;
          if( sig.userid ) args.userid = sig.userid;
        }
        if(self.send('control', 'subscribe', args)) {
          subscription.subscribe();
          process.nextTick(function() {
            self.bbt.emit('subscribed', subscription);
          });
          return self.bbt;
        }else {
            self.bbt.emit('subscribeError', new Error('Unexpected error encountered while subscribing'), args);
            return self.bbt;
        }
      }else {
        self.bbt.emit('subscribeError', new Error('Unsupported authentication method!'), args);
        return self.bbt;
      }
    } else {
      self.bbt.emit('subscribeError', new Error('Connection error encountered'), args);
      return self.bbt;
    }
  }else {
    if(self.send('control', 'subscribe', args)) {
      subscription.subscribe();
      self.bbt.emit('subscribed', subscription);
      return self.bbt;
    }else {
      self.bbt.emit('subscribeError', new Error('Unexpected error encountered while subscribing'), args);
      return self.bbt;
    }
  }
}

Connection.prototype.unsubscribe = function(subscription) {
  var self = this;
  if(subscription) {
    subscription.unsubscribe();
    if( this.send('control', 'unsubscribe', {channel: subscription.channel, resource: subscription.resource }) ) {
      self.bbt.emit('unsubscribed', {channel: subscription.channel, resource: subscription.resource });
    } else {
      self.bbt.emit('unsubscribeError', new Error('Unexpected error encountered while unsubscribing'), {channel: subscription.channel, resource: subscription.resource });
    }
  }
  return self.bbt;
}

Connection.prototype.publish = function(args) {
  var self = this;
  var subscription = this.bbt.getSubscriptionWithPermission(args.channel, args.resource, false, true);

  if(subscription && subscription.hasWritePermission()) {
    if(this.send('stream', 'emit', args)) {
      self.bbt.emit('published', args);
    }else {
      self.bbt.emit('publishError', new Error('Error while publishing message'), args);
    }
  } else {
    self.bbt.emit('publishError', 'Permission error: cant\'t publish on the given resource!', args);
  }
  return self.bbt;
}

Connection.prototype.write = function(args) {
  var self = this;
  var subscription = self.bbt.getSubscriptionWithPermission(args.channel, args.resource, false, true);

  if(subscription && subscription.hasWritePermission()) {
    if(this.send('stream', 'write', args)) {
      self.bbt.emit('written', args);
    }else {
      self.bbt.emit('writeError', new Error('Error while sending write message'), args);
    }
  }else {
    self.bbt.emit('writeError', new Error('Permission error: cant\'t write on the given resource!'), args);
  }
  return self.bbt;
}

//For internal use only
Connection.prototype.send = function(cname, evt, data) {
  if(this.connection) {
    this.connection.json.send({version: PROTO, channel: cname, event: evt, data: data});
    return true;
  }else {
    return false;
  }
}

module.exports = Connection
<commit_msg>Fix #21: enable reconnection after disconnect<commit_after>"use strict";

var request = require('request')
  , crypto = require('crypto')
  , querystring = require('querystring')
  , url = require('url');

var PROTO = 1; //Version of Beebotte Protocol

/** @constructor */
function Connection (bbt) {
  this.bbt = bbt;
  this.connection = null;
  this.connected;
}

Connection.prototype.onConnection = function() {
  for(var c in this.bbt.subscriptions) {
    this.do_subscribe(this.bbt.subscriptions[c]);
  }
}

Connection.prototype.getWsUrl = function() {
  var p = (this.bbt.transport.ssl === true)? this.bbt.transport.sport : this.bbt.transport.port;
  return ((this.bbt.transport.ssl === true)? 'https://' : 'http://' ) + this.bbt.transport.ws_host + ':' + p;
}

Connection.prototype.connect = function () {

  var self = this;

  // already connected, return
  if (self.connection && self.connection.connected) {
    return self;
  }

  if (self.connection) {
    self.connection.connect()
  } else {
    if( self.bbt.transport.token ) {
      var query =  'token=' + self.bbt.transport.token + '&username=' + (self.bbt.userinfo.username || '');
    } else {
      var query =  'key=' + self.bbt.transport.key + '&username=' + (self.bbt.userinfo.username || '');
    }
    self.connection = require('socket.io-client')(self.getWsUrl(), {query: query });
  }
  self.connection.on('connect', function () {
    if(self.connected === false) {
      self.connected = true;
      self.onConnection();
      self.bbt.emit('reconnected');
    } else {
      self.connected = true;
      self.bbt.emit('connected');
    }
  });

  self.connection.on('error', function (err) {
    self.bbt.emit('error', err);
  });

  self.connection.on('disconnect', function () {
    self.connected = false;
    self.bbt.emit('disconnected');
  });

  self.connection.on('message', function (msg) {
    if(msg.channel && msg.resource) {
      var subscription = self.bbt.getAnySubscription(msg.channel, msg.resource);
      if(subscription) {
        subscription.fct(msg);
      }else {
        //console.log('Warning! non subscribed message: ' + JSON.stringify(msg));
      }
    } else {
      //console.log('Warning! non conform message: ' + JSON.stringify(msg));
    }
  });

  self.connection.on('getsid', function (sid) {
    self._id = sid
  })

  return self.bbt;
}

Connection.prototype.disconnect = function () {
  if(this.connection.connected) {
    this.connection.disconnect();
    this.connection.removeAllListeners();
  };
  return this.bbt;
}

//for internal use only
Connection.prototype.send_auth = function(sig, source) {
  var self = this;
  if(self.send('control', 'authenticate', {auth: sig.auth, source: source})) {
    this.authenticated = true;
    return true;
  }else {
    this.authenticated = false
    return false;
  }
}

Connection.prototype.subscribe = function(args, callback) {
  var subscription = this.bbt.getSubscription(args.channel, args.resource);

  if(subscription) {
    if(subscription.update(args)) this.do_subscribe(subscription);
  }else {
    subscription = this.bbt.addSubscription(args, callback);
    this.do_subscribe(subscription);
  }
  return this.bbt;
}

Connection.prototype.do_subscribe = function(subscription) {
  var self = this;
  if(!self.connection.connected) return self.bbt;
  var args = {};
  args.channel = subscription.channel;
  args.resource = subscription.resource || self.bbt.wildcard;
  args.ttl = args.ttl || 0;
  args.read = subscription.read;
  args.write = subscription.write;
  if(typeof self.bbt.userinfo !== 'undefined') {
    args.userinfo = self.bbt.userinfo;
  }
  //Authentication required for write access and for read access to private or presence resources
  function authNeeded() {
    if(args.write === true) return true;
    if(args.channel.indexOf('private-') === 0) return true;
    if(args.channel.indexOf('presence-') === 0) return true;
    return false;
  }

  /* Utility function for signing connection
   * Will be used with function based authentication method
   * (self sufficient authentication - requires Secret Key)
   */
  function signSubscription ( ) {
    var to_sign = args.sid + ':' + args.channel + '.' + args.resource + ':ttl=' + args.ttl + ':read=' + args.read + ':write=' + args.write;

    if( self.bbt.userinfo.userid ) to_sign = to_sign + ':userid=' + self.bbt.userinfo.userid;

    var signature = crypto.createHmac('sha1', self.bbt.transport.secretKey).update(to_sign).digest('base64');
    var auth = { auth: self.bbt.transport.key + ':' + signature };

    if( self.bbt.userinfo.userid ) auth.userid = self.bbt.userinfo.userid;

    return auth;
  }

  if( authNeeded() ) {
    if( ! self.bbt.transport.auth_endpoint && self.bbt.transport.auth_method !== 'fct' ) {
      self.bbt.emit('subscribeError', new Error('Authentication error: Missing authentication endpoint!'), args);
      return self.bbt;
    }
    if(self.connection && self.connection.connected && self.connection.io.engine.id) {
      args.sid = self.connection.io.engine.id;
      if(self.bbt.transport.auth_method === 'get') {

        var uri = self.bbt.transport.auth_endpoint;
        var options = {
            url: url.parse(uri),
            qs: args,
            method: 'GET',
        }

        request(options, function (error, response, body) {
            if(error) {
              self.bbt.emit('subscribeError', error, args);
              return self.bbt;
            }
            if (!error && response.statusCode == 200) {
              if( typeof body === 'string' ) {
                try {
                  body = JSON.parse(body);
                } catch(e) {
                  self.bbt.emit('subscribeError', new Error('Unable to authenticate client'), args);
                  return self.bbt;
                }
              }
              args.sig = body.auth;
              if( body.userid ) args.userid = body.userid;
              if(self.send('control', 'subscribe', args)) {
                subscription.subscribe();
                self.bbt.emit('subscribed', subscription);
                return self.bbt;
              }else {
                  self.bbt.emit('subscribeError', new Error('Unexpected error encountered while subscribing'), args);
                  return self.bbt;
              }
            }else {
                self.bbt.emit('subscribeError', new Error('Unable to authenticate client'), args);
                return self.bbt;
            }
        });
      }else if (self.bbt.transport.auth_method === 'post') {
        uri = self.bbt.transport.auth_endpoint;
        options = {
          url: url.parse(uri),
          method: 'POST',
          body: JSON.stringify(args)
        }

        options.headers = {
          'Content-Type': 'application/json'
        };

        request(options, function (error, response, body) {
            if(error) {
              self.bbt.emit('subscribeError', new Error('Unable to authenticate client'), args);
              return self.bbt;
            }
            if (!error && response.statusCode == 200) {
              if( typeof body === 'string' ) {
                try {
                  body = JSON.parse(body);
                } catch(e) {
                  self.bbt.emit('subscribeError', new Error('Unable to authenticate client'), args);
                  return self.bbt;
                }
              }
              args.sig = body.auth;
              if( body.userid ) args.userid = body.userid;
              if(self.send('control', 'subscribe', args)) {
                subscription.subscribe();
                self.bbt.emit('subscribed', subscription);
                return self.bbt;
              }else {
                  self.bbt.emit('subscribeError', new Error('Unexpected error encountered while subscribing'), args);
                  return self.bbt;
              }
            }else {
              self.bbt.emit('subscribeError', new Error('Unable to authenticate client'), args);
              return self.bbt;
            }
        });
      }else if (self.bbt.transport.auth_method === 'fct') {
        if( self.bbt.transport.token ) {
          args.token = self.bbt.transport.token;
          if( self.bbt.userinfo.userid ) args.userid = self.bbt.userinfo.userid;
        } else {
          var sig = signSubscription( );
          if( !sig ) {
            self.bbt.emit('subscribeError', new Error('Unable to authenticate client'), args);
            return self.bbt;
          }
          args.sig = sig.auth;
          if( sig.userid ) args.userid = sig.userid;
        }
        if(self.send('control', 'subscribe', args)) {
          subscription.subscribe();
          process.nextTick(function() {
            self.bbt.emit('subscribed', subscription);
          });
          return self.bbt;
        }else {
            self.bbt.emit('subscribeError', new Error('Unexpected error encountered while subscribing'), args);
            return self.bbt;
        }
      }else {
        self.bbt.emit('subscribeError', new Error('Unsupported authentication method!'), args);
        return self.bbt;
      }
    } else {
      self.bbt.emit('subscribeError', new Error('Connection error encountered'), args);
      return self.bbt;
    }
  }else {
    if(self.send('control', 'subscribe', args)) {
      subscription.subscribe();
      self.bbt.emit('subscribed', subscription);
      return self.bbt;
    }else {
      self.bbt.emit('subscribeError', new Error('Unexpected error encountered while subscribing'), args);
      return self.bbt;
    }
  }
}

Connection.prototype.unsubscribe = function(subscription) {
  var self = this;
  if(subscription) {
    subscription.unsubscribe();
    if( this.send('control', 'unsubscribe', {channel: subscription.channel, resource: subscription.resource }) ) {
      self.bbt.emit('unsubscribed', {channel: subscription.channel, resource: subscription.resource });
    } else {
      self.bbt.emit('unsubscribeError', new Error('Unexpected error encountered while unsubscribing'), {channel: subscription.channel, resource: subscription.resource });
    }
  }
  return self.bbt;
}

Connection.prototype.publish = function(args) {
  var self = this;
  var subscription = this.bbt.getSubscriptionWithPermission(args.channel, args.resource, false, true);

  if(subscription && subscription.hasWritePermission()) {
    if(this.send('stream', 'emit', args)) {
      self.bbt.emit('published', args);
    }else {
      self.bbt.emit('publishError', new Error('Error while publishing message'), args);
    }
  } else {
    self.bbt.emit('publishError', 'Permission error: cant\'t publish on the given resource!', args);
  }
  return self.bbt;
}

Connection.prototype.write = function(args) {
  var self = this;
  var subscription = self.bbt.getSubscriptionWithPermission(args.channel, args.resource, false, true);

  if(subscription && subscription.hasWritePermission()) {
    if(this.send('stream', 'write', args)) {
      self.bbt.emit('written', args);
    }else {
      self.bbt.emit('writeError', new Error('Error while sending write message'), args);
    }
  }else {
    self.bbt.emit('writeError', new Error('Permission error: cant\'t write on the given resource!'), args);
  }
  return self.bbt;
}

//For internal use only
Connection.prototype.send = function(cname, evt, data) {
  if(this.connection) {
    this.connection.json.send({version: PROTO, channel: cname, event: evt, data: data});
    return true;
  }else {
    return false;
  }
}

module.exports = Connection
<|endoftext|>