<commit_before>var gmail3 = function () {
  var gmail3 = {};
  var msgConsumers = [];

  function addMsgConsumer(msgConsumer) {
    msgConsumers.push(msgConsumer);
  }

  function isEmpty(db, sheet) {
    var rows = objDB.getRows(db, sheet, [], {}, 1);
    if (rows.length === 0) {
      return true;
    } else {
      return false;
    }
  }

  function emailReport(scriptState, msgConsumers) {
    var db = objDB.open(scriptState.ssID);
    Logger.log('Sending last months report via Email');
    for(i = 0; i < msgConsumers.length; i += 1) {
      if (! msgConsumers[i].isEmpty(db)) {
        msgConsumers[i].graphFunction(scriptState);
      }
    }
    var htmlBody = "";
    var inlineImages = {};
    var blob;

    for(i = 0; i < msgConsumers.length; i += 1) {
      if (! msgConsumers[i].isEmpty(db)) {
        htmlBody += "
\n";
        blob = msgConsumers[i].chart.getAs('image/png').setName(msgConsumers[i].name + ".png");
        inlineImages[i] = blob;
      }
    }

    MailApp.sendEmail({
      to: Session.getActiveUser().getEmail(),
      subject: "[gmail3] - " + "Gmail usage analysis for " + scriptState.monthToProcess.format('MMMM YYYY'),
      htmlBody: htmlBody,
      inlineImages: inlineImages
    });
    scriptState.reportEmailed = true;
  }

  function createSpreadSheet(monthToProcess) {
    var ssNew = SpreadsheetApp.create("gmail3 Data - " + monthToProcess.format('MMM YYYY'));
    var ssID = ssNew.getUrl().split('/')[7];
    // This is needed so we can use the Google Visualization API Query Language
    // to set the data source URL for graphs
    allowPublicViewing(ssID);
    return ssID;
  }

  function initializeSpreadSheet(scriptState) {
    var ss = SpreadsheetApp.openById(scriptState.ssID);
    for(i = 0; i < msgConsumers.length; i += 1) {
      msgConsumers[i].initSpreadSheet(ss);
    }
  }

  function populateData(scriptState, msgConsumers) {
    var db = objDB.open(scriptState.ssID);
    Logger.log('Populating Data');
    // Process up to 100 gmail threads at once
    var batchSize = 100;
    if (scriptState.debugEnabled) {
      // If we have debugEnabled only process 20, so runs are quicker
      batchSize = 20;
    }
    var after = scriptState.monthToProcess.clone().startOf('month').format('YYYY/MM/DD');
    var before = scriptState.monthToProcess.clone().endOf('month').add(1, 'days').format('YYYY/MM/DD');
    var query = "in:anywhere -label:sms -label:call-log -label:chats -label:spam -filename:ics";
    query += " -from:maestro.bounces.google.com -from:unified-notifications.bounces.google.com -from:docs.google.com";
    query += " -from:group.calendar.google.com -from:apps-scripts-notifications@google.com";
    query += " -from:sites.bounces.google.com";
    query += " after:" + after + " before:" + before;

    Logger.log('Searching: ' + query);
    var threads = GmailApp.search(query, scriptState.threadIndex, batchSize);
    if (scriptState.debugEnabled && scriptState.threadIndex >= 40) {
      scriptState.dataPopulated = true;
    } else if (threads.length === 0) {
      scriptState.dataPopulated = true;
    } else {
      for (thread = 0; thread < threads.length; thread += 1) {
        var msg = threads[thread].getMessages()[0];
        for(var i = 0; i < msgConsumers.length; i += 1) {
          if (scriptState.debugEnabled) {
            Logger.log('Calling Message function for, ' + msgConsumers[i].name);
          }
          try {
            msgConsumers[i].msgFunction(msg, db);
          } catch (e) {
            throw new Error("msgConsumer, " + msgConsumers[i].name + ", threw error: " + e.message);
          }
        }
      }
    }
    scriptState.threadIndex += batchSize;
    Logger.log('PopulateData run complete: threads: ' + threads.length + ', threadIndex: ' + scriptState.threadIndex);
  }

  function setScriptState(scriptState) {
    var scriptStateString = JSON.stringify(scriptState);
    PropertiesService.getUserProperties().setProperty("scriptState", scriptStateString);
    Logger.log("New scriptState set: " + scriptStateString);
  }

  function getScriptState() {
    var scriptState = null;
    var nextMonthToProcess;
    var scriptStateString = PropertiesService.getUserProperties().getProperty("scriptState");
    var debugEnabledUserProperty = PropertiesService.getUserProperties().getProperty("debugEnabled");
    if (debugEnabledUserProperty === "true") {
      debugEnabled = true;
      Logger.log("Debugging enabled");
    } else {
      debugEnabled = false;
      Logger.log("Debugging disabled");
    }
    Logger.log("Retrieved scriptState: " + scriptStateString);
    if (scriptStateString === null) {
      Logger.log('No stored scriptState, creating an empty one');
      scriptState = {};
      scriptState.debugEnabled = debugEnabled;
      var currentMonth = moment().startOf('month');
      if (scriptState.debugEnabled) {
        nextMonthToProcess = currentMonth.clone().startOf('year');
      } else {
        nextMonthToProcess = currentMonth.clone().subtract(1, 'days').startOf('month');
      }
      scriptState.ssID = createSpreadSheet(nextMonthToProcess);
      initializeSpreadSheet(scriptState);
      scriptState.monthToProcess = nextMonthToProcess;
      scriptState.dataPopulated = false;
      scriptState.reportEmailed = false;
      scriptState.threadIndex = 0;
      scriptState.triggers = [];
      scriptState.dailyRunningTimeTotalSeconds = 0;
      scriptState.currentDay = moment().startOf('day');
    } else {
      scriptState = JSON.parse(scriptStateString);
      scriptState.debugEnabled = debugEnabled;
      scriptState.monthToProcess = moment(scriptState.monthToProcess);
      scriptState.currentDay = moment(scriptState.currentDay);
    }

    // Reset the running total, if it is a new day
    if (! moment().startOf('day').isSame(scriptState.currentDay)) {
      scriptState.dailyRunningTimeTotalSeconds = 0;
      scriptState.currentDay = moment().startOf('day');
    }

    // Create a daily trigger if it does not exist
    dailyTrigger = _.find(scriptState.triggers, function (trigger) {
      return trigger.type === "atHour";
    });
    if (dailyTrigger === undefined) {
      Logger.log("No daily trigger, adding one");
      trigger = ScriptApp.newTrigger("main")
      .timeBased()
      .atHour(1)
      .everyDays(1)
      .create();
      scriptState.triggers.push({
        type: "atHour",
        uniqueId: trigger.getUniqueId(),
      });
    }

    return scriptState;
  }

  function deleteSheet1(scriptState) {
    var ss = SpreadsheetApp.openById(scriptState.ssID);
    if (ss.getSheetByName("Sheet1") !== null) {
      ss.deleteSheet(ss.getSheetByName("Sheet1"));
    }
  }

  function incrementMonth(scriptState) {
    Logger.log('Processing done for the current month, incrementing the month');
    var nextMonthToProcess = scriptState.monthToProcess.clone()
    .endOf('month')
    .add(1, 'days')
    .startOf('month');
    // This may not suceeed so do it first, before changing the script state
    scriptState.ssID = createSpreadSheet(nextMonthToProcess);
    initializeSpreadSheet(scriptState);
    scriptState.monthToProcess = nextMonthToProcess;
    scriptState.dataPopulated = false;
    scriptState.reportEmailed = false;
    scriptState.threadIndex = 0;
    scriptState.dailyRunningTimeTotalSeconds = 0;
    scriptState.currentDay = moment().startOf('day');

    return scriptState;
  }

  function deleteAtTriggers(scriptState) {
    Logger.log("Deleting all existing at triggers");
    var trigger;
    function uniqueIdMatchProjectTrigger(projectTrigger) {
      return function (trigger) {
        return trigger.uniqueId === projectTrigger.getUniqueId();
      };
    }
    function uniqueIdMatch(uniqueId) {
      return function (trigger) {
        return trigger.uniqueId === uniqueId;
      };
    }
    var projectTriggers = ScriptApp.getProjectTriggers();
    for (var i = 0; i < projectTriggers.length; i++) {
      trigger = _.find(scriptState.triggers, uniqueIdMatchProjectTrigger(projectTriggers[i]));
      if (trigger === undefined) {
        // We have no record of this trigger, so delete it
        ScriptApp.deleteTrigger(projectTriggers[i]);
      } else {
        if (trigger.type === "at") {
          var uniqueId = projectTriggers[i].getUniqueId();
          ScriptApp.deleteTrigger(projectTriggers[i]);
          scriptState.triggers = _.reject(scriptState.triggers, uniqueIdMatch(uniqueId));
        }
      }
    }
  }

  function setupNextAtTrigger(scriptState) {
    // Delete any previous at triggers, so we don't exceed the trigger quota
    deleteAtTriggers(scriptState);

    // If less than daily run time, 3 hours, and there is still time left in the
    // day, add a new trigger
    if (scriptState.dailyRunningTimeTotalSeconds < (170 * 60) &&
      moment().add(35, 'minutes').date() === moment().date()) {
      Logger.log("Scheduling a new at trigger at: " + moment().add(15, "minutes").toISOString());
      trigger = ScriptApp.newTrigger("main").timeBased().after(15 * 60 * 1000).create();
      scriptState.triggers.push({
        type: "at",
        uniqueId: trigger.getUniqueId(),
      });
    } else {
      Logger.log("Not scheduling any further triggers for today to avoid exceeding execution quota of 3 hours");
      Logger.log("Running time total today " + (scriptState.dailyRunningTimeTotalSeconds / 60) + " minutes");
    }
  }

  function extractEmailAddress(emailAddress) {
    var emailAddressRegex = /^(?:.*<)?([^<>]+)(?:>)?$/;
    return emailAddressRegex.exec(emailAddress)[1].toLowerCase();
  }

  function unfoldHeaders(foldedHeaders) {
    var unfoldRegex = /\r\n[ \t]+/g;
    return foldedHeaders.replace(unfoldRegex, " ");
  }

  function extractHeaders(rawMessage) {
    var headers = {};
    var parseHeaderRegex = /^[ \n\t]*([^:]+):[ \t]*(.*)$/;
    var extractHeadersRegex = /\r\n\r\n/;
    var printableRegex = /[^ -~]+/g;
    var foldedHeaders = rawMessage.split(extractHeadersRegex)[0];
    var unfoldedHeaders = unfoldHeaders(foldedHeaders);
    var headerLines = unfoldedHeaders.split(/\r\n/);
    for (var i = 0; i < headerLines.length; i++) {
      var printableHeader = headerLines[i].replace(printableRegex, "");
      var headerMatch = parseHeaderRegex.exec(printableHeader);
      if (headerMatch === null) {
        throw new Error("headerMatch returned null");
      }
      if (headerMatch[1] !== "") {
        var headerField = headerMatch[1].toLowerCase();
        if (headers.hasOwnProperty(headerField)) {
          headers[headerField].push(headerMatch[2]);
        } else {
          headers[headerField] = [headerMatch[2]];
        }
      }
    }
    return headers;
  }

  function allowPublicViewing(ID) {
    file = DriveApp.getFileById(ID);
    file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
  }

  // These are admittedly swiss cheese tests for robots, but I am at loss to
  // find better methods.
  function emailSentByRobot(rawMessage) {
    var msgFromRobot = false;
    var headers = extractHeaders(rawMessage);
    var robotMailerRegex = /\s\(Postfix/;
    var fromRobots = [
      /^no-?reply@.*$/,
      /^alert@pingdom.com$/,
    ];
    if (headers.hasOwnProperty("received")) {
      var firstHop = headers.received[headers.received.length - 1];
      if (robotMailerRegex.test(firstHop)) {
        msgFromRobot = true;
      }
    }
    for (var i = 0; i < fromRobots.length; i++) {
      if (fromRobots[i].test(headers.from)) {
        msgFromRobot = true;
        break;
      }
    }
    return msgFromRobot;
  }

  function main() {
    var lock = LockService.getScriptLock();
    if (!lock.tryLock(1000)) {
      Logger.log('Could not obtain lock after a second');
      return;
    }
    var currentMonth = moment().startOf('month');
    var scriptStartTime = moment();
    var scriptState = null;

    try {
      scriptState = getScriptState();
    } catch (e) {
      Logger.log('Error: ' + e.message);
      return;
    }

    if (scriptState.monthToProcess.isBefore(currentMonth)) {
      Logger.log("Beginning processing of: gmail3 - " + scriptState.monthToProcess.format('MMM YYYY'));
      if (scriptState.dataPopulated) {
        if (scriptState.reportEmailed) {
          try {
            deleteSheet1(scriptState);
          } catch (e) {
            Logger.log('Error: Unable to delete Sheet1 for the previous month: ' + e.message);
          }
          try {
            incrementMonth(scriptState);
          } catch (e) {
            Logger.log('Error: Unable to increment month: ' + e.message);
          }
        } else {
          try {
            emailReport(scriptState, msgConsumers);
          } catch (e) {
            Logger.log('Error: Unable to email report for the previous month: ' + e.message);
          }
        }
      } else {
        try {
          populateData(scriptState, msgConsumers);
        } catch (e) {
          Logger.log('Error: Unable to populate data: ' + e.message);
        }
      }
      scriptState.dailyRunningTimeTotalSeconds += moment().diff(scriptStartTime, 'seconds');
      try {
        setupNextAtTrigger(scriptState);
      } catch (e) {
        Logger.log('Error: Unable to schedule next at trigger: ' + e.message);
      }
    } else {
      Logger.log('Previous month processed complete, nothing todo');
    }

    for (var i = 0; i < 5; i++) {
      try {
        setScriptState(scriptState);
        break;
      } catch (e) {
        Logger.log('Error: Unable to set script state: ' + e.message);
      }
    }
    lock.releaseLock();
  }

  gmail3.emailReport = emailReport;
  gmail3.addMsgConsumer = addMsgConsumer;
  gmail3.createSpreadSheet = createSpreadSheet;
  gmail3.initializeSpreadSheet = initializeSpreadSheet;
  gmail3.isEmpty = isEmpty;
  gmail3.populateData = populateData;
  gmail3.setScriptState = setScriptState;
  gmail3.getScriptState = getScriptState;
  gmail3.deleteSheet1 = deleteSheet1;
  gmail3.incrementMonth = incrementMonth;
  gmail3.deleteAtTriggers = deleteAtTriggers;
  gmail3.setupNextAtTrigger = setupNextAtTrigger;
  gmail3.extractEmailAddress = extractEmailAddress;
  gmail3.unfoldHeaders = unfoldHeaders;
  gmail3.extractHeaders = extractHeaders;
  gmail3.allowPublicViewing = allowPublicViewing;
  gmail3.emailSentByRobot = emailSentByRobot;
  gmail3.main = main;

  return gmail3;
}();
<commit_msg>Use a user lock rather than a script lock<commit_after>var gmail3 = function () {
  var gmail3 = {};
  var msgConsumers = [];

  function addMsgConsumer(msgConsumer) {
    msgConsumers.push(msgConsumer);
  }

  function isEmpty(db, sheet) {
    var rows = objDB.getRows(db, sheet, [], {}, 1);
    if (rows.length === 0) {
      return true;
    } else {
      return false;
    }
  }

  function emailReport(scriptState, msgConsumers) {
    var db = objDB.open(scriptState.ssID);
    Logger.log('Sending last months report via Email');
    for(i = 0; i < msgConsumers.length; i += 1) {
      if (! msgConsumers[i].isEmpty(db)) {
        msgConsumers[i].graphFunction(scriptState);
      }
    }
    var htmlBody = "";
    var inlineImages = {};
    var blob;

    for(i = 0; i < msgConsumers.length; i += 1) {
      if (! msgConsumers[i].isEmpty(db)) {
        htmlBody += "
\n";
        blob = msgConsumers[i].chart.getAs('image/png').setName(msgConsumers[i].name + ".png");
        inlineImages[i] = blob;
      }
    }

    MailApp.sendEmail({
      to: Session.getActiveUser().getEmail(),
      subject: "[gmail3] - " + "Gmail usage analysis for " + scriptState.monthToProcess.format('MMMM YYYY'),
      htmlBody: htmlBody,
      inlineImages: inlineImages
    });
    scriptState.reportEmailed = true;
  }

  function createSpreadSheet(monthToProcess) {
    var ssNew = SpreadsheetApp.create("gmail3 Data - " + monthToProcess.format('MMM YYYY'));
    var ssID = ssNew.getUrl().split('/')[7];
    // This is needed so we can use the Google Visualization API Query Language
    // to set the data source URL for graphs
    allowPublicViewing(ssID);
    return ssID;
  }

  function initializeSpreadSheet(scriptState) {
    var ss = SpreadsheetApp.openById(scriptState.ssID);
    for(i = 0; i < msgConsumers.length; i += 1) {
      msgConsumers[i].initSpreadSheet(ss);
    }
  }

  function populateData(scriptState, msgConsumers) {
    var db = objDB.open(scriptState.ssID);
    Logger.log('Populating Data');
    // Process up to 100 gmail threads at once
    var batchSize = 100;
    if (scriptState.debugEnabled) {
      // If we have debugEnabled only process 20, so runs are quicker
      batchSize = 20;
    }
    var after = scriptState.monthToProcess.clone().startOf('month').format('YYYY/MM/DD');
    var before = scriptState.monthToProcess.clone().endOf('month').add(1, 'days').format('YYYY/MM/DD');
    var query = "in:anywhere -label:sms -label:call-log -label:chats -label:spam -filename:ics";
    query += " -from:maestro.bounces.google.com -from:unified-notifications.bounces.google.com -from:docs.google.com";
    query += " -from:group.calendar.google.com -from:apps-scripts-notifications@google.com";
    query += " -from:sites.bounces.google.com";
    query += " after:" + after + " before:" + before;

    Logger.log('Searching: ' + query);
    var threads = GmailApp.search(query, scriptState.threadIndex, batchSize);
    if (scriptState.debugEnabled && scriptState.threadIndex >= 40) {
      scriptState.dataPopulated = true;
    } else if (threads.length === 0) {
      scriptState.dataPopulated = true;
    } else {
      for (thread = 0; thread < threads.length; thread += 1) {
        var msg = threads[thread].getMessages()[0];
        for(var i = 0; i < msgConsumers.length; i += 1) {
          if (scriptState.debugEnabled) {
            Logger.log('Calling Message function for, ' + msgConsumers[i].name);
          }
          try {
            msgConsumers[i].msgFunction(msg, db);
          } catch (e) {
            throw new Error("msgConsumer, " + msgConsumers[i].name + ", threw error: " + e.message);
          }
        }
      }
    }
    scriptState.threadIndex += batchSize;
    Logger.log('PopulateData run complete: threads: ' + threads.length + ', threadIndex: ' + scriptState.threadIndex);
  }

  function setScriptState(scriptState) {
    var scriptStateString = JSON.stringify(scriptState);
    PropertiesService.getUserProperties().setProperty("scriptState", scriptStateString);
    Logger.log("New scriptState set: " + scriptStateString);
  }

  function getScriptState() {
    var scriptState = null;
    var nextMonthToProcess;
    var scriptStateString = PropertiesService.getUserProperties().getProperty("scriptState");
    var debugEnabledUserProperty = PropertiesService.getUserProperties().getProperty("debugEnabled");
    if (debugEnabledUserProperty === "true") {
      debugEnabled = true;
      Logger.log("Debugging enabled");
    } else {
      debugEnabled = false;
      Logger.log("Debugging disabled");
    }
    Logger.log("Retrieved scriptState: " + scriptStateString);
    if (scriptStateString === null) {
      Logger.log('No stored scriptState, creating an empty one');
      scriptState = {};
      scriptState.debugEnabled = debugEnabled;
      var currentMonth = moment().startOf('month');
      if (scriptState.debugEnabled) {
        nextMonthToProcess = currentMonth.clone().startOf('year');
      } else {
        nextMonthToProcess = currentMonth.clone().subtract(1, 'days').startOf('month');
      }
      scriptState.ssID = createSpreadSheet(nextMonthToProcess);
      initializeSpreadSheet(scriptState);
      scriptState.monthToProcess = nextMonthToProcess;
      scriptState.dataPopulated = false;
      scriptState.reportEmailed = false;
      scriptState.threadIndex = 0;
      scriptState.triggers = [];
      scriptState.dailyRunningTimeTotalSeconds = 0;
      scriptState.currentDay = moment().startOf('day');
    } else {
      scriptState = JSON.parse(scriptStateString);
      scriptState.debugEnabled = debugEnabled;
      scriptState.monthToProcess = moment(scriptState.monthToProcess);
      scriptState.currentDay = moment(scriptState.currentDay);
    }

    // Reset the running total, if it is a new day
    if (! moment().startOf('day').isSame(scriptState.currentDay)) {
      scriptState.dailyRunningTimeTotalSeconds = 0;
      scriptState.currentDay = moment().startOf('day');
    }

    // Create a daily trigger if it does not exist
    dailyTrigger = _.find(scriptState.triggers, function (trigger) {
      return trigger.type === "atHour";
    });
    if (dailyTrigger === undefined) {
      Logger.log("No daily trigger, adding one");
      trigger = ScriptApp.newTrigger("main")
      .timeBased()
      .atHour(1)
      .everyDays(1)
      .create();
      scriptState.triggers.push({
        type: "atHour",
        uniqueId: trigger.getUniqueId(),
      });
    }

    return scriptState;
  }

  function deleteSheet1(scriptState) {
    var ss = SpreadsheetApp.openById(scriptState.ssID);
    if (ss.getSheetByName("Sheet1") !== null) {
      ss.deleteSheet(ss.getSheetByName("Sheet1"));
    }
  }

  function incrementMonth(scriptState) {
    Logger.log('Processing done for the current month, incrementing the month');
    var nextMonthToProcess = scriptState.monthToProcess.clone()
    .endOf('month')
    .add(1, 'days')
    .startOf('month');
    // This may not suceeed so do it first, before changing the script state
    scriptState.ssID = createSpreadSheet(nextMonthToProcess);
    initializeSpreadSheet(scriptState);
    scriptState.monthToProcess = nextMonthToProcess;
    scriptState.dataPopulated = false;
    scriptState.reportEmailed = false;
    scriptState.threadIndex = 0;
    scriptState.dailyRunningTimeTotalSeconds = 0;
    scriptState.currentDay = moment().startOf('day');

    return scriptState;
  }

  function deleteAtTriggers(scriptState) {
    Logger.log("Deleting all existing at triggers");
    var trigger;
    function uniqueIdMatchProjectTrigger(projectTrigger) {
      return function (trigger) {
        return trigger.uniqueId === projectTrigger.getUniqueId();
      };
    }
    function uniqueIdMatch(uniqueId) {
      return function (trigger) {
        return trigger.uniqueId === uniqueId;
      };
    }
    var projectTriggers = ScriptApp.getProjectTriggers();
    for (var i = 0; i < projectTriggers.length; i++) {
      trigger = _.find(scriptState.triggers, uniqueIdMatchProjectTrigger(projectTriggers[i]));
      if (trigger === undefined) {
        // We have no record of this trigger, so delete it
        ScriptApp.deleteTrigger(projectTriggers[i]);
      } else {
        if (trigger.type === "at") {
          var uniqueId = projectTriggers[i].getUniqueId();
          ScriptApp.deleteTrigger(projectTriggers[i]);
          scriptState.triggers = _.reject(scriptState.triggers, uniqueIdMatch(uniqueId));
        }
      }
    }
  }

  function setupNextAtTrigger(scriptState) {
    // Delete any previous at triggers, so we don't exceed the trigger quota
    deleteAtTriggers(scriptState);

    // If less than daily run time, 3 hours, and there is still time left in the
    // day, add a new trigger
    if (scriptState.dailyRunningTimeTotalSeconds < (170 * 60) &&
      moment().add(35, 'minutes').date() === moment().date()) {
      Logger.log("Scheduling a new at trigger at: " + moment().add(15, "minutes").toISOString());
      trigger = ScriptApp.newTrigger("main").timeBased().after(15 * 60 * 1000).create();
      scriptState.triggers.push({
        type: "at",
        uniqueId: trigger.getUniqueId(),
      });
    } else {
      Logger.log("Not scheduling any further triggers for today to avoid exceeding execution quota of 3 hours");
      Logger.log("Running time total today " + (scriptState.dailyRunningTimeTotalSeconds / 60) + " minutes");
    }
  }

  function extractEmailAddress(emailAddress) {
    var emailAddressRegex = /^(?:.*<)?([^<>]+)(?:>)?$/;
    return emailAddressRegex.exec(emailAddress)[1].toLowerCase();
  }

  function unfoldHeaders(foldedHeaders) {
    var unfoldRegex = /\r\n[ \t]+/g;
    return foldedHeaders.replace(unfoldRegex, " ");
  }

  function extractHeaders(rawMessage) {
    var headers = {};
    var parseHeaderRegex = /^[ \n\t]*([^:]+):[ \t]*(.*)$/;
    var extractHeadersRegex = /\r\n\r\n/;
    var printableRegex = /[^ -~]+/g;
    var foldedHeaders = rawMessage.split(extractHeadersRegex)[0];
    var unfoldedHeaders = unfoldHeaders(foldedHeaders);
    var headerLines = unfoldedHeaders.split(/\r\n/);
    for (var i = 0; i < headerLines.length; i++) {
      var printableHeader = headerLines[i].replace(printableRegex, "");
      var headerMatch = parseHeaderRegex.exec(printableHeader);
      if (headerMatch === null) {
        throw new Error("headerMatch returned null");
      }
      if (headerMatch[1] !== "") {
        var headerField = headerMatch[1].toLowerCase();
        if (headers.hasOwnProperty(headerField)) {
          headers[headerField].push(headerMatch[2]);
        } else {
          headers[headerField] = [headerMatch[2]];
        }
      }
    }
    return headers;
  }

  function allowPublicViewing(ID) {
    file = DriveApp.getFileById(ID);
    file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
  }

  // These are admittedly swiss cheese tests for robots, but I am at loss to
  // find better methods.
  function emailSentByRobot(rawMessage) {
    var msgFromRobot = false;
    var headers = extractHeaders(rawMessage);
    var robotMailerRegex = /\s\(Postfix/;
    var fromRobots = [
      /^no-?reply@.*$/,
      /^alert@pingdom.com$/,
    ];
    if (headers.hasOwnProperty("received")) {
      var firstHop = headers.received[headers.received.length - 1];
      if (robotMailerRegex.test(firstHop)) {
        msgFromRobot = true;
      }
    }
    for (var i = 0; i < fromRobots.length; i++) {
      if (fromRobots[i].test(headers.from)) {
        msgFromRobot = true;
        break;
      }
    }
    return msgFromRobot;
  }

  function main() {
    var lock = LockService.getUserLock();
    if (!lock.tryLock(1000)) {
      Logger.log('Could not obtain lock after a second');
      return;
    }
    var currentMonth = moment().startOf('month');
    var scriptStartTime = moment();
    var scriptState = null;

    try {
      scriptState = getScriptState();
    } catch (e) {
      Logger.log('Error: ' + e.message);
      return;
    }

    if (scriptState.monthToProcess.isBefore(currentMonth)) {
      Logger.log("Beginning processing of: gmail3 - " + scriptState.monthToProcess.format('MMM YYYY'));
      if (scriptState.dataPopulated) {
        if (scriptState.reportEmailed) {
          try {
            deleteSheet1(scriptState);
          } catch (e) {
            Logger.log('Error: Unable to delete Sheet1 for the previous month: ' + e.message);
          }
          try {
            incrementMonth(scriptState);
          } catch (e) {
            Logger.log('Error: Unable to increment month: ' + e.message);
          }
        } else {
          try {
            emailReport(scriptState, msgConsumers);
          } catch (e) {
            Logger.log('Error: Unable to email report for the previous month: ' + e.message);
          }
        }
      } else {
        try {
          populateData(scriptState, msgConsumers);
        } catch (e) {
          Logger.log('Error: Unable to populate data: ' + e.message);
        }
      }
      scriptState.dailyRunningTimeTotalSeconds += moment().diff(scriptStartTime, 'seconds');
      try {
        setupNextAtTrigger(scriptState);
      } catch (e) {
        Logger.log('Error: Unable to schedule next at trigger: ' + e.message);
      }
    } else {
      Logger.log('Previous month processed complete, nothing todo');
    }

    for (var i = 0; i < 5; i++) {
      try {
        setScriptState(scriptState);
        break;
      } catch (e) {
        Logger.log('Error: Unable to set script state: ' + e.message);
      }
    }
    lock.releaseLock();
  }

  gmail3.emailReport = emailReport;
  gmail3.addMsgConsumer = addMsgConsumer;
  gmail3.createSpreadSheet = createSpreadSheet;
  gmail3.initializeSpreadSheet = initializeSpreadSheet;
  gmail3.isEmpty = isEmpty;
  gmail3.populateData = populateData;
  gmail3.setScriptState = setScriptState;
  gmail3.getScriptState = getScriptState;
  gmail3.deleteSheet1 = deleteSheet1;
  gmail3.incrementMonth = incrementMonth;
  gmail3.deleteAtTriggers = deleteAtTriggers;
  gmail3.setupNextAtTrigger = setupNextAtTrigger;
  gmail3.extractEmailAddress = extractEmailAddress;
  gmail3.unfoldHeaders = unfoldHeaders;
  gmail3.extractHeaders = extractHeaders;
  gmail3.allowPublicViewing = allowPublicViewing;
  gmail3.emailSentByRobot = emailSentByRobot;
  gmail3.main = main;

  return gmail3;
}();
<|endoftext|>


<commit_before>'use strict';
//
// test/unit/filters/filtersSpec.js
//
describe('Unit: Testing Filters', function() {

  beforeEach(module('copayApp.filters'));

  describe('limitAddress', function() {

    it('should handle emtpy list', inject(function($filter) {
      var limitAddress = $filter('limitAddress');
      expect(limitAddress([], false)).to.be.empty;
    }));

    it('should honor show all', inject(function($filter) {
      var limitAddress = $filter('limitAddress');
      var addresses = [{}, {}, {}, {}, {}];
      expect(limitAddress(addresses, true).length).to.equal(5);
      expect(limitAddress([{}], false).length).to.equal(1);
    }));

    it('should filter correctly', inject(function($filter) {
      var limitAddress = $filter('limitAddress');
      var addresses = [{
        isChange: true,
        balance: 0
      }, {
        isChange: false,
        balance: 0
      }, {
        isChange: true,
        balance: 0
      }, {
        isChange: false,
        balance: 0
      }, {
        isChange: true,
        balance: 0
      }, {
        isChange: false,
        balance: 0
      }, {
        isChange: true,
        balance: 0
      }, {
        isChange: false,
        balance: 0
      }];
      expect(limitAddress(addresses, false).length).to.equal(1);

      addresses[0].isChange = false;
      expect(limitAddress(addresses, false).length).to.equal(2);

      addresses[2].isChange = false;
      expect(limitAddress(addresses, false).length).to.equal(3);

      addresses[3].isChange = false;
      expect(limitAddress(addresses, false).length).to.equal(3);

      addresses[0].balance = 20;
      expect(limitAddress(addresses, false).length).to.equal(3);

      addresses[0].balance = 20;
      expect(limitAddress(addresses, false).length).to.equal(3);

      addresses[7].balance = 20;
      expect(limitAddress(addresses, false).length).to.equal(4);
    }));
  });
});
<commit_msg>Test for the new filter<commit_after>'use strict';
//
// test/unit/filters/filtersSpec.js
//
describe('Unit: Testing Filters', function() {

  beforeEach(module('copayApp.filters'));

  describe('limitAddress', function() {

    it('should handle emtpy list', inject(function($filter) {
      var limitAddress = $filter('limitAddress');
      expect(limitAddress([], false)).to.be.empty;
    }));

    it('should honor show all', inject(function($filter) {
      var limitAddress = $filter('limitAddress');
      var addresses = [{}, {}, {}, {}, {}];
      expect(limitAddress(addresses, true).length).to.equal(5);
      expect(limitAddress([{}], false).length).to.equal(1);
    }));

    it('should filter correctly', inject(function($filter) {
      var limitAddress = $filter('limitAddress');
      var addresses = [{
        isChange: true,
        balance: 0
      }, {
        isChange: false,
        balance: 0
      }, {
        isChange: true,
        balance: 0
      }, {
        isChange: false,
        balance: 0
      }, {
        isChange: true,
        balance: 0
      }, {
        isChange: false,
        balance: 0
      }, {
        isChange: true,
        balance: 0
      }, {
        isChange: false,
        balance: 0
      }];
      expect(limitAddress(addresses, false).length).to.equal(1);

      addresses[0].isChange = false;
      expect(limitAddress(addresses, false).length).to.equal(2);

      addresses[2].isChange = false;
      expect(limitAddress(addresses, false).length).to.equal(3);

      addresses[3].isChange = false;
      expect(limitAddress(addresses, false).length).to.equal(3);

      addresses[0].balance = 20;
      expect(limitAddress(addresses, false).length).to.equal(3);

      addresses[0].balance = 20;
      expect(limitAddress(addresses, false).length).to.equal(3);

      addresses[7].balance = 20;
      expect(limitAddress(addresses, false).length).to.equal(4);
    }));
  });

  describe('noFractionNumber', function() {
    it('should format number to display correctly', inject(function($filter) {
      var noFraction = $filter('noFractionNumber');
      var fraction = 4;
      expect(noFraction(3100, fraction)).to.equal('3,100');
      expect(noFraction(3100200, fraction)).to.equal('3,100,200');
      expect(noFraction(3, fraction)).to.equal('3');
      expect(noFraction(0.3, fraction)).to.equal(0.3);
    }));
  });

});
<|endoftext|>


<commit_before>(typeof window === 'undefined' ? describe : describe.skip)('timers inside sandbox', function () {
    this.timeout(1000 * 60);
    var Sandbox = require('../../lib'),
        ctx;

    beforeEach(function (done) {
        Sandbox.createContext({debug: true}, function (err, context) {
            if (err) { return done(err); }
            ctx = context;
            done();
        });
    });

    afterEach(function () {
        ctx.dispose();
        ctx = null;
    });

    it('must work with setTimeout inside sandbox', function (done) {
        ctx.execute(`
            var startTime = Date.now();
            setTimeout(function () {
                pm.globals.set('timeout', Date.now() - startTime, 'number');
            }, 100);
        `, {
            timeout: 200
        }, function (err, res) {
            if (err) { return done(err); }

            expect(err).to.not.be.ok();
            expect(res.return.async).to.be(true);

            expect(res).to.have.property('globals');
            expect(res.globals.values).to.be.an('array');
            expect(res.globals.values[0].value).to.be.greaterThan(95);
            done();
        });
    });

    it('must be able to clear timeout', function (done) {
        var timeoutExecuted = 'timeout not executed';

        ctx.on('console', function (cursor, level, message) { // keep track of intervals passed
            if (message === 'timeout') { timeoutExecuted = 'timeout executed'; }
        });

        ctx.execute(`
            var id = setTimeout(function () {
                console.log('timeout');
            }, 100);
            clearTimeout(id);
        `, {
            // debug: false,
            timeout: 200
        }, function (err, res) {
            if (err) { return done(err); }

            expect(err).to.not.be.ok();
            expect(res.return.async).to.be(false);

            // we wait for a while to ensure that the timeout was actually cleared.
            setTimeout(function () {
                expect(timeoutExecuted).to.eql('timeout not executed');
                done();
            }, 150);
        });
    });

    it('must work with setImmediate inside sandbox', function (done) {
        ctx.execute(`
            var startTime = Date.now();
            setImmediate(function () {
                pm.globals.set('executed', 1, 'boolean');
            });
        `, {
            timeout: 200
        }, function (err, res) {
            if (err) { return done(err); }

            expect(err).to.not.be.ok();
            expect(res.return.async).to.be(true);

            expect(res).to.have.property('globals');
            expect(res.globals.values).to.be.an('array');
            expect(res.globals.values[0].value).to.be(true);
            done();
        });
    });

    it('must be able to clear immediates', function (done) {
        var status = 'not executed';

        ctx.on('console', function (cursor, level, message) { // keep track of executions passed
            if (message === 'executed') { status = 'executed'; }
        });

        ctx.execute(`
            var id = setImmediate(function () {
                console.log('executed');
            });
            clearImmediate(id);
        `, {
            debug: false,
            timeout: 200
        }, function (err, res) {
            if (err) { return done(err); }

            expect(err).to.not.be.ok();
            expect(res.return.async).to.be(false);

            // we wait for a while to ensure that the timeout was actually cleared.
            setTimeout(function () {
                expect(status).to.eql('not executed');
                done();
            }, 150);
        });
    });

    it('must time out if timers run beyond interval and stop the interval', function (done) {
        var count = {
            terminal: null,
            current: 0
        };

        ctx.on('console', function (cursor, level, message) { // keep track of intervals passed
            if (message === 'interval') { count.current++; }
        });

        ctx.execute(`
            var startTime = Date.now();
            setInterval(function () {
                console.log('interval')
            }, 50);
        `, {
            debug: false,
            timeout: 125
        }, function (err, res) {
            count.terminal = count.current;

            expect(err).to.have.property('name', 'Error');
            expect(err).to.have.property('message', 'sandbox: asynchronous script execution timeout');
            expect(res.return.async).to.be(true);

            // now wait for a while to ensure no extra interval timers were fired
            setTimeout(function () {
                expect(count).to.have.property('current', count.terminal);
                done();
            }, 250);
        });
    });

    it('must be able to clear intervals and exit', function (done) {
        var status = 0;

        ctx.on('console', function (cursor, level, message) { // keep track of executions passed
            if (message === 'executed') { status += 1; }
        });

        ctx.execute(`
            var id = setInterval(function () {
                console.log('executed');
            }, 25);
            setTimeout(function() {
                clearInterval(id);
            }, 155);
        `, {
            debug: false,
            timeout: 200
        }, function (err, res) {
            if (err) { return done(err); }
            var currentCount = status;

            expect(err).to.not.be.ok();
            expect(res.return.async).to.be(true);
            expect(currentCount).to.be.above(0);

            setTimeout(function () {
                expect(status).to.be(currentCount);
                done();
            }, 200);
        });
    });

    it('should wait for the longest running timer if multiple ones are running', function (done) {
        var order = [],
            e1 = 'executed1',
            e2 = 'executed2';

        ctx.on('console', function (cursor, level, message) { // keep track of executions passed
            if (message === e1) {
                order.push(e1);
            }
            else if (message === e2) {
                order.push(e2);
            }
            else {
                order.push('error'); // this will ensure the test fails
            }
        });

        ctx.execute(`
            setTimeout(function () {
                console.log('${e1}');
            }, 25);

            setTimeout(function(executed2) {
                console.log('${e2}');
            }, 100)
        `, {
            debug: false,
            timeout: 200
        }, function (err, res) {
            if (err) { return done(err); }
            setTimeout(function () {
                expect(res.return.async).to.be(true);
                expect(order).to.eql([e1, e2]);
                done();
            }, 200);
        });
    });

    it('should ensure that no timers are called after the timeout is reached', function (done) {
        var order = [],
            e1 = 'executed1',
            e2 = 'executed2';

        ctx.on('console', function (cursor, level, message) { // keep track of executions passed
            if (message === e1) {
                order.push(e1);
            }
            else if (message === e2) {
                order.push(e2);
            }
            else {
                order.push('error'); // this will ensure the test fails
            }
        });

        ctx.execute(`
            setTimeout(function () {
                console.log('${e1}');
            }, 25);

            setTimeout(function(executed2) {
                console.log('${e2}'); // this should never run.
            }, 300)
        `, {
            debug: false,
            timeout: 200
        }, function (err, res) {
            setTimeout(function () {
                expect(err).to.be.ok();
                expect(err.message).to.match(/timeout/);

                expect(res.return.async).to.be(true);
                expect(order).to.eql([e1]);
                done();
            }, 500);
        });
    });

    it('should allow setting timers inside timers', function (done) {
        var msg = 'message-from-the-other-side',
            status = false;

        ctx.on('console', function (cursor, level, message) { // keep track of executions passed
            if (message === msg && !status) {
                // only do this if the status is not already true, to ensure a single call.
                status = true;
            }
            else {
                status = false;
            }
        });

        ctx.execute(`
            setTimeout(function () {
                setTimeout(function() {
                    console.log('${msg}');
                }, 25);
            }, 25);
        `, {
            debug: false,
            timeout: 200
        }, function (err, res) {
            if (err) { return done(err); }

            setTimeout(function () {
                expect(res.return.async).to.be(true);
                expect(status).to.be(true);
                done();
            }, 500);
        });
    });

    it('should allow async tests', function (done) {
        var testName = 'postman-sb-test',
            result;

        ctx.on('execution.assertion', function (execution, tests) {
            result = tests[0];
        });

        ctx.execute(`
            setTimeout(function () {
                pm.test('${testName}', function () {
                    pm.expect(100).to.eql(100);
                });
            }, 25);
        `, {
            debug: false,
            timeout: 200
        }, function (err, execution) {
            if (err) { return done(err); }

            setTimeout(function () {
                expect(execution.return.async).to.be(true);
                expect(result).to.eql({
                    name: 'postman-sb-test',
                    skipped: false,
                    passed: true,
                    error: null,
                    index: 0
                });
                done();
            }, 500);
        });
    });
});
<commit_msg>Update sandbox-timers tests to chai<commit_after>(typeof window === 'undefined' ? describe : describe.skip)('timers inside sandbox', function () {
    this.timeout(1000 * 60);
    var Sandbox = require('../../lib'),
        ctx;

    beforeEach(function (done) {
        Sandbox.createContext({debug: true}, function (err, context) {
            if (err) { return done(err); }
            ctx = context;
            done();
        });
    });

    afterEach(function () {
        ctx.dispose();
        ctx = null;
    });

    it('should work with setTimeout inside sandbox', function (done) {
        ctx.execute(`
            var startTime = Date.now();
            setTimeout(function () {
                pm.globals.set('timeout', Date.now() - startTime, 'number');
            }, 100);
        `, {
            timeout: 200
        }, function (err, res) {
            if (err) { return done(err); }

            expect(err).to.be.null;
            expect(res.return.async).to.be.true;

            expect(res).to.have.property('globals').that.has.property('values').that.is.an('array');
            expect(res.globals.values[0].value).to.be.greaterThan(95);
            done();
        });
    });

    it('should be able to clear timeout', function (done) {
        var timeoutExecuted = 'timeout not executed';

        ctx.on('console', function (cursor, level, message) { // keep track of intervals passed
            if (message === 'timeout') { timeoutExecuted = 'timeout executed'; }
        });

        ctx.execute(`
            var id = setTimeout(function () {
                console.log('timeout');
            }, 100);
            clearTimeout(id);
        `, {
            // debug: false,
            timeout: 200
        }, function (err, res) {
            if (err) { return done(err); }

            expect(err).to.be.null;
            expect(res.return.async).to.be.false;

            // we wait for a while to ensure that the timeout was actually cleared.
            setTimeout(function () {
                expect(timeoutExecuted).to.eql('timeout not executed');
                done();
            }, 150);
        });
    });

    it('should work with setImmediate inside sandbox', function (done) {
        ctx.execute(`
            var startTime = Date.now();
            setImmediate(function () {
                pm.globals.set('executed', 1, 'boolean');
            });
        `, {
            timeout: 200
        }, function (err, res) {
            if (err) { return done(err); }

            expect(err).to.be.null;
            expect(res.return.async).to.be.true;

            expect(res).to.have.property('globals').that.has.property('values').that.is.an('array');
            expect(res.globals.values[0].value).to.be.true;
            done();
        });
    });

    it('should be able to clear immediates', function (done) {
        var status = 'not executed';

        ctx.on('console', function (cursor, level, message) { // keep track of executions passed
            if (message === 'executed') { status = 'executed'; }
        });

        ctx.execute(`
            var id = setImmediate(function () {
                console.log('executed');
            });
            clearImmediate(id);
        `, {
            debug: false,
            timeout: 200
        }, function (err, res) {
            if (err) { return done(err); }

            expect(err).to.be.null;
            expect(res.return.async).to.be.false;

            // we wait for a while to ensure that the timeout was actually cleared.
            setTimeout(function () {
                expect(status).to.eql('not executed');
                done();
            }, 150);
        });
    });

    it('should time out if timers run beyond interval and stop the interval', function (done) {
        var count = {
            terminal: null,
            current: 0
        };

        ctx.on('console', function (cursor, level, message) { // keep track of intervals passed
            if (message === 'interval') { count.current++; }
        });

        ctx.execute(`
            var startTime = Date.now();
            setInterval(function () {
                console.log('interval')
            }, 50);
        `, {
            debug: false,
            timeout: 125
        }, function (err, res) {
            count.terminal = count.current;

            expect(err).to.deep.include({
                name: 'Error',
                message: 'sandbox: asynchronous script execution timeout'
            });
            expect(res.return.async).to.be.true;

            // now wait for a while to ensure no extra interval timers were fired
            setTimeout(function () {
                expect(count).to.have.property('current', count.terminal);
                done();
            }, 250);
        });
    });

    it('should be able to clear intervals and exit', function (done) {
        var status = 0;

        ctx.on('console', function (cursor, level, message) { // keep track of executions passed
            if (message === 'executed') { status += 1; }
        });

        ctx.execute(`
            var id = setInterval(function () {
                console.log('executed');
            }, 25);
            setTimeout(function() {
                clearInterval(id);
            }, 155);
        `, {
            debug: false,
            timeout: 200
        }, function (err, res) {
            if (err) { return done(err); }
            var currentCount = status;

            expect(err).to.be.null;
            expect(res.return.async).to.be.true;
            expect(currentCount).to.be.above(0);

            setTimeout(function () {
                expect(status).to.equal(currentCount);
                done();
            }, 200);
        });
    });

    it('should wait for the longest running timer if multiple ones are running', function (done) {
        var order = [],
            e1 = 'executed1',
            e2 = 'executed2';

        ctx.on('console', function (cursor, level, message) { // keep track of executions passed
            if (message === e1) {
                order.push(e1);
            }
            else if (message === e2) {
                order.push(e2);
            }
            else {
                order.push('error'); // this will ensure the test fails
            }
        });

        ctx.execute(`
            setTimeout(function () {
                console.log('${e1}');
            }, 25);

            setTimeout(function(executed2) {
                console.log('${e2}');
            }, 100)
        `, {
            debug: false,
            timeout: 200
        }, function (err, res) {
            if (err) { return done(err); }
            setTimeout(function () {
                expect(res.return.async).to.be.true;
                expect(order).to.eql([e1, e2]);
                done();
            }, 200);
        });
    });

    it('should ensure that no timers are called after the timeout is reached', function (done) {
        var order = [],
            e1 = 'executed1',
            e2 = 'executed2';

        ctx.on('console', function (cursor, level, message) { // keep track of executions passed
            if (message === e1) {
                order.push(e1);
            }
            else if (message === e2) {
                order.push(e2);
            }
            else {
                order.push('error'); // this will ensure the test fails
            }
        });

        ctx.execute(`
            setTimeout(function () {
                console.log('${e1}');
            }, 25);

            setTimeout(function(executed2) {
                console.log('${e2}'); // this should never run.
            }, 300)
        `, {
            debug: false,
            timeout: 200
        }, function (err, res) {
            setTimeout(function () {
                expect(err).to.be.ok;
                expect(err.message).to.match(/timeout/);

                expect(res.return.async).to.be.true;
                expect(order).to.eql([e1]);
                done();
            }, 500);
        });
    });

    it('should allow setting timers inside timers', function (done) {
        var msg = 'message-from-the-other-side',
            status = false;

        ctx.on('console', function (cursor, level, message) { // keep track of executions passed
            if (message === msg && !status) {
                // only do this if the status is not already true, to ensure a single call.
                status = true;
            }
            else {
                status = false;
            }
        });

        ctx.execute(`
            setTimeout(function () {
                setTimeout(function() {
                    console.log('${msg}');
                }, 25);
            }, 25);
        `, {
            debug: false,
            timeout: 200
        }, function (err, res) {
            if (err) { return done(err); }

            setTimeout(function () {
                expect(res.return.async).to.be.true;
                expect(status).to.be.true;
                done();
            }, 500);
        });
    });

    it('should allow async tests', function (done) {
        var testName = 'postman-sb-test',
            result;

        ctx.on('execution.assertion', function (execution, tests) {
            result = tests[0];
        });

        ctx.execute(`
            setTimeout(function () {
                pm.test('${testName}', function () {
                    pm.expect(100).to.eql(100);
                });
            }, 25);
        `, {
            debug: false,
            timeout: 200
        }, function (err, execution) {
            if (err) { return done(err); }

            setTimeout(function () {
                expect(execution.return.async).to.be.true;
                expect(result).to.eql({
                    name: 'postman-sb-test',
                    skipped: false,
                    passed: true,
                    error: null,
                    index: 0
                });
                done();
            }, 500);
        });
    });
});
<|endoftext|>


<commit_before>// Sort by length

'use strict';

module.exports = function (a, b) {
	return a.length - b.length;
};
<commit_msg>Sort by length now uses 'property' sort function<commit_after>// Sort by length

'use strict';

module.exports =  require('./property')('length');
<|endoftext|>


<commit_before>'use strict';
/**
 * Simple module to localize the React interface using the same syntax
 * used in the ReactNativeLocalization module
 * (https://github.com/stefalda/ReactNativeLocalization)
 *
 * Originally developed by Stefano Falda (stefano.falda@gmail.com)
 *
 * It uses a call to the Navigator/Browser object to get the current interface language,
 * then display the correct language strings or the default language (the first
 * one if a match is not found).
 *
 * How to use:
 * Check the instructions at:
 * https://github.com/stefalda/react-localization
 */
Object.defineProperty(exports, "__esModule", {
  value: true
});

var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var LocalizedStrings = (function () {
  _createClass(LocalizedStrings, [{
    key: "_getBestMatchingLanguage",
    value: function _getBestMatchingLanguage(language, props) {
      //If an object with the passed language key exists return it
      if (props[language]) return language;
      //if the string is composed try to find a match with only the first language identifiers (en-US --> en)
      var idx = language.indexOf("-");
      if (idx >= 0) {
        language = language.substring(0, idx);
        if (props[language]) return language;
      }
      //Return the default language (the first coded)
      return Object.keys(props)[0];
    }
  }]);

  function LocalizedStrings(props) {
    _classCallCheck(this, LocalizedStrings);

    var interfaceLanguage = navigator.language || navigator.userLanguage;
    //Store locally the passed strings
    this.props = props;
    //Set language to its default value (the interface)
    this.setLanguage(interfaceLanguage);
  }

  //Can be used from ouside the class to force a particular language
  //indipendently from the interface one

  _createClass(LocalizedStrings, [{
    key: "setLanguage",
    value: function setLanguage(language) {
      //Check if exists a translation for the current language or if the default
      //should be used
      var bestLanguage = this._getBestMatchingLanguage(language, this.props);
      this.language = bestLanguage;
      //Associate the language object to the this object
      if (this.props[bestLanguage]) {
        //console.log("There are strings for the language:"+language);
        var localizedStrings = this.props[this.language];
        for (var key in localizedStrings) {
          //console.log("Checking property:"+key);
          if (localizedStrings.hasOwnProperty(key)) {
            //console.log("Associating property:"+key);
            this[key] = localizedStrings[key];
          }
        }
      }
    }

    //The current language displayed (could differ from the interface language
    // if it has been forced manually and a matching translation has been found)
  }, {
    key: "getLanguage",
    value: function getLanguage() {
      return this.language;
    }

    //The current interface language (could differ from the language displayed)
  }, {
    key: "getInterfaceLanguage",
    value: function getInterfaceLanguage() {
      return interfaceLanguage;
    }

    //Return an array containing the available languages passed as props in the constructor
  }, {
    key: "getAvailableLanguages",
    value: function getAvailableLanguages() {
      if (!this.availableLanguages) {
        this.availableLanguages = [];
        for (var language in this.props) {
          this.availableLanguages.push(language);
        }
      }
      return this.availableLanguages;
    }

    //Format the passed string replacing the numbered placeholders
    //i.e. I'd like some {0} and {1}, or just {0}
    //Use example:
    //  strings.formatString(strings.question, strings.bread, strings.butter)
  }, {
    key: "formatString",
    value: function formatString(str) {
      var res = str;

      for (var _len = arguments.length, values = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
        values[_key - 1] = arguments[_key];
      }

      for (var i = 0; i < values.length; i++) {
        res = this._replaceAll("{" + i + "}", values[i], res);
      }
      return res;
    }

    //Replace all occorrencies of a string in another using RegExp
  }, {
    key: "_replaceAll",
    value: function _replaceAll(find, replace, str) {
      //Escape find
      find = find.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
      return str.replace(new RegExp(find, 'g'), replace);
    }
  }]);

  return LocalizedStrings;
})();

exports["default"] = LocalizedStrings;
module.exports = exports["default"];<commit_msg>Added default to 'en-US' when navigator.language or navigator.interfaceLanuguage is nor present<commit_after>'use strict';
/**
 * Simple module to localize the React interface using the same syntax
 * used in the ReactNativeLocalization module
 * (https://github.com/stefalda/ReactNativeLocalization)
 *
 * Originally developed by Stefano Falda (stefano.falda@gmail.com)
 *
 * It uses a call to the Navigator/Browser object to get the current interface language,
 * then display the correct language strings or the default language (the first
 * one if a match is not found).
 *
 * How to use:
 * Check the instructions at:
 * https://github.com/stefalda/react-localization
 */
Object.defineProperty(exports, "__esModule", {
  value: true
});

var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })();

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var LocalizedStrings = (function () {
  _createClass(LocalizedStrings, [{
    key: "_getBestMatchingLanguage",
    value: function _getBestMatchingLanguage(language, props) {
      //If an object with the passed language key exists return it
      if (props[language]) return language;
      //if the string is composed try to find a match with only the first language identifiers (en-US --> en)
      var idx = language.indexOf("-");
      if (idx >= 0) {
        language = language.substring(0, idx);
        if (props[language]) return language;
      }
      //Return the default language (the first coded)
      return Object.keys(props)[0];
    }
  }]);

  function LocalizedStrings(props) {
    _classCallCheck(this, LocalizedStrings);

    var interfaceLanguage = navigator.language || navigator.userLanguage || 'en-US';
    //Store locally the passed strings
    this.props = props;
    //Set language to its default value (the interface)
    this.setLanguage(interfaceLanguage);
  }

  //Can be used from ouside the class to force a particular language
  //indipendently from the interface one

  _createClass(LocalizedStrings, [{
    key: "setLanguage",
    value: function setLanguage(language) {
      //Check if exists a translation for the current language or if the default
      //should be used
      var bestLanguage = this._getBestMatchingLanguage(language, this.props);
      this.language = bestLanguage;
      //Associate the language object to the this object
      if (this.props[bestLanguage]) {
        //console.log("There are strings for the language:"+language);
        var localizedStrings = this.props[this.language];
        for (var key in localizedStrings) {
          //console.log("Checking property:"+key);
          if (localizedStrings.hasOwnProperty(key)) {
            //console.log("Associating property:"+key);
            this[key] = localizedStrings[key];
          }
        }
      }
    }

    //The current language displayed (could differ from the interface language
    // if it has been forced manually and a matching translation has been found)
  }, {
    key: "getLanguage",
    value: function getLanguage() {
      return this.language;
    }

    //The current interface language (could differ from the language displayed)
  }, {
    key: "getInterfaceLanguage",
    value: function getInterfaceLanguage() {
      return interfaceLanguage;
    }

    //Return an array containing the available languages passed as props in the constructor
  }, {
    key: "getAvailableLanguages",
    value: function getAvailableLanguages() {
      if (!this.availableLanguages) {
        this.availableLanguages = [];
        for (var language in this.props) {
          this.availableLanguages.push(language);
        }
      }
      return this.availableLanguages;
    }

    //Format the passed string replacing the numbered placeholders
    //i.e. I'd like some {0} and {1}, or just {0}
    //Use example:
    //  strings.formatString(strings.question, strings.bread, strings.butter)
  }, {
    key: "formatString",
    value: function formatString(str) {
      var res = str;

      for (var _len = arguments.length, values = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) {
        values[_key - 1] = arguments[_key];
      }

      for (var i = 0; i < values.length; i++) {
        res = this._replaceAll("{" + i + "}", values[i], res);
      }
      return res;
    }

    //Replace all occorrencies of a string in another using RegExp
  }, {
    key: "_replaceAll",
    value: function _replaceAll(find, replace, str) {
      //Escape find
      find = find.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1");
      return str.replace(new RegExp(find, 'g'), replace);
    }
  }]);

  return LocalizedStrings;
})();

exports["default"] = LocalizedStrings;
module.exports = exports["default"];
<|endoftext|>


<commit_before>'use strict';

var chai = require('chai');
var expect = chai.expect;
var sinon = require("sinon");

var rewire = require('../../rewire_helper');
var MenuView = rewire('views/MenuView');

window.plugPro = {};
var templateLoader = require('../../template_loader');
Backbone.$ = $;

describe("MenuView", function(){

	var JST;
	var menuView;
	var fakeToggleSettings;

	beforeEach(function(){
		MenuView.__set__( "ToggleSettingView", Backbone.View.extend() );

		window.plugPro.JST = templateLoader([
			'menu.html',
			'toggle_setting.html'
		]);

		$('body').html("

");
		fakeToggleSettings = {
			"toggleSettings": {
				"fakeSetting1": {},
				"fakeSetting2": {}
			}
		};

		menuView = new MenuView( fakeToggleSettings );
		$('#app-menu').append( menuView.$el );

	});

	describe("initialize", function(){

		it("should add toggleSettings to list", function(){
			expect( menuView.settingViews.length ).to.equal(2);
		});

		it("should load menu template", function(){

			sinon.spy( window.plugPro.JST, 'menu.html' );

			menuView = new MenuView( fakeToggleSettings );
			$('#app-menu').append( menuView.$el );

			expect( window.plugPro.JST['menu.html'].calledOnce ).to.be.true;
		});

	});

	describe("render", function(){

		beforeEach(function(){
			sinon.stub( menuView, 'syncWithMainPlugMenu' );
		});

		it("should call syncWithMainPlugMenu", function(){
			menuView.render();

			expect( menuView.syncWithMainPlugMenu.calledOnce ).to.equal( true );
		});

		it("should render all settingViews", function(){
			menuView.settingViews.forEach( function( setting ){
				sinon.stub( setting, "render");
			});

			menuView.render();

			menuView.settingViews.forEach( function( setting ){
				expect( setting.render.calledOnce ).to.equal( true );
			});

		});

	});

	describe("updateVisibility", function(){

		it("should show and hide", function(){
			menuView.render();

			$('.list').css('left', '0px');
			menuView.updateVisibility();
			expect( $('#plugpro-menu').hasClass('expanded') ).to.be.true;

			$('.list').css('left', '10px');
			menuView.updateVisibility();
			expect( $('#plugpro-menu').hasClass('expanded') ).to.be.false;
		});

	});

});<commit_msg>Add MenuView tests<commit_after>'use strict';

var chai = require('chai');
var expect = chai.expect;
var sinon = require("sinon");

var rewire = require('../../rewire_helper');
var MenuView = rewire('views/MenuView');

window.plugPro = {};
var templateLoader = require('../../template_loader');
Backbone.$ = $;

describe("MenuView", function(){

	var JST;
	var menuView;
	var fakeToggleSettings;

	beforeEach(function(){
		MenuView.__set__( "ToggleSettingView", Backbone.View.extend() );

		window.plugPro.JST = templateLoader([
			'menu.html',
			'toggle_setting.html'
		]);

		$('body').html("

");
		fakeToggleSettings = {
			"toggleSettings": {
				"fakeSetting1": {},
				"fakeSetting2": {}
			}
		};

		menuView = new MenuView( fakeToggleSettings );
		$('#app-menu').append( menuView.$el );

	});

	describe("initialize", function(){

		it("should add toggleSettings to list", function(){
			expect( menuView.settingViews.length ).to.equal(2);
		});

		it("should load menu template", function(){

			sinon.spy( window.plugPro.JST, 'menu.html' );

			menuView = new MenuView( fakeToggleSettings );
			$('#app-menu').append( menuView.$el );

			expect( window.plugPro.JST['menu.html'].calledOnce ).to.be.true;
		});

	});

	describe("render", function(){

		beforeEach(function(){
			sinon.stub( menuView, 'syncWithMainPlugMenu' );
		});

		it("should call syncWithMainPlugMenu", function(){
			menuView.render();

			expect( menuView.syncWithMainPlugMenu.calledOnce ).to.equal( true );
		});

		it("should render all settingViews", function(){
			menuView.settingViews.forEach( function( setting ){
				sinon.stub( setting, "render");
			});

			menuView.render();

			menuView.settingViews.forEach( function( setting ){
				expect( setting.render.calledOnce ).to.equal( true );
			});

		});

	});

	describe("syncWithMainPlugMenu", function(){

		var fakeTransitionEvent;

		beforeEach(function(){
			fakeTransitionEvent = 'webkitTransitionEnd';
			sinon.stub( menuView, "getTransitionEvent", function(){
				return fakeTransitionEvent;
			});
			sinon.stub( menuView, "updateVisibility", function(){} );
		});

		afterEach(function(){
			menuView.getTransitionEvent.restore();
			menuView.updateVisibility.restore();
		});

		it("should add transition listener to call updateVisibility()", function(){

			sinon.stub(
				$('#app-menu')
				.find('.list')[0],
				"addEventListener",
				function( evt, callback ){
					callback();
				}
			);
			menuView.syncWithMainPlugMenu();
			expect( menuView.updateVisibility.calledOnce ).to.equal( true );
		});

		describe("mousedown listener", function(){

			afterEach(function(){
				document.addEventListener.restore();
			});

			it("should not close menu when clicking Pro menu", function( done ){

				var fakeEvent = {
					target: menuView.el,
					stopPropagation: function(){
						done();
					}
				};

				sinon.stub( document, "addEventListener",
					function( evt, callback ){
						expect( evt ).to.equal("mousedown");
						callback( fakeEvent );
					}
				);
				menuView.syncWithMainPlugMenu();

			});

			it("should not close menu when clicking child of Pro menu", function( done ){
				menuView.$el.append('

');

				var fakeEvent = {
					target: $('#plugpro-menu-child')[0],
					stopPropagation: function(){
						done();
					}
				};

				sinon.stub( document, "addEventListener",
					function( evt, callback ){
						expect( evt ).to.equal("mousedown");
						callback( fakeEvent );
					}
				);
				menuView.syncWithMainPlugMenu();

			});

			it("should propagate if not pro menu", function( done ){
				menuView.$el.append('

');

				var fakeEl = document.createElement('div');

				var fakeEvent = {
					target: fakeEl,
					stopPropagation: sinon.stub()
				};

				sinon.stub( document, "addEventListener",
					function( evt, callback ){
						expect( evt ).to.equal("mousedown");
						callback( fakeEvent );

						expect( fakeEvent.stopPropagation.called ).to.be.false;
						done();
					}
				);
				menuView.syncWithMainPlugMenu();

			});

		});

	});

	describe("updateVisibility", function(){

		it("should show and hide", function(){
			menuView.render();

			$('.list').css('left', '0px');
			menuView.updateVisibility();
			expect( $('#plugpro-menu').hasClass('expanded') ).to.be.true;

			$('.list').css('left', '10px');
			menuView.updateVisibility();
			expect( $('#plugpro-menu').hasClass('expanded') ).to.be.false;
		});

	});

	describe("getTransitionEvent", function(){

		it("should return correct transition", function(){
			var transition = menuView.getTransitionEvent({
				style: {
					"WebkitAnimation": "WebkitAnimation"
				}
			});

			expect( transition ).to.equal( "webkitTransitionEnd" );
		});

	});

});<|endoftext|>


<commit_before>/*
	Neurons Kit

	Copyright (c) 2015 - 2020 Cédric Ronvel

	The MIT License (MIT)

	Permission is hereby granted, free of charge, to any person obtaining a copy
	of this software and associated documentation files (the "Software"), to deal
	in the Software without restriction, including without limitation the rights
	to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
	copies of the Software, and to permit persons to whom the Software is
	furnished to do so, subject to the following conditions:

	The above copyright notice and this permission notice shall be included in all
	copies or substantial portions of the Software.

	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
	IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
	AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
	LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
	SOFTWARE.
*/

"use strict" ;

/*
	Sort of genetic algorithm without gene, since coding a neural network have little sens.

	So this algorithm should work this way:
		* Concurrently test all network and give them a score
		* Select the best
		* Among the best, clone some of them and mutate them:
			* Change randomly some network weights
			* Remove some useless synapse, or whole neuron if its last synapse is removed
				* If a synapse is rarely used (most of time its excitation is closed to zero)
				* If the output signal is most of time closed to zero
			* Eventually add a synapse/neuron, if the network has not enough of them (how to detect that?)
				* If two neurons activate at the same time, it is said that they should connect
		* Cross 2 networks:
			* Clone the first network, create a training sample from the response of the second network
			* Train the clone with the training sample
			* Alternatively (since both network are probably structurally identical to each other at the begining),
				make the training sample have some of the first network response too
			* Crossing cost a lot of CPU, it should be used with care, probably only with the best of the best
*/

<commit_msg>ideas<commit_after>/*
	Neurons Kit

	Copyright (c) 2015 - 2020 Cédric Ronvel

	The MIT License (MIT)

	Permission is hereby granted, free of charge, to any person obtaining a copy
	of this software and associated documentation files (the "Software"), to deal
	in the Software without restriction, including without limitation the rights
	to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
	copies of the Software, and to permit persons to whom the Software is
	furnished to do so, subject to the following conditions:

	The above copyright notice and this permission notice shall be included in all
	copies or substantial portions of the Software.

	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
	IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
	AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
	LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
	SOFTWARE.
*/

"use strict" ;

/*
	Sort of genetic algorithm without gene.
	Inspired by NEAT:
	http://nn.cs.utexas.edu/downloads/papers/stanley.ec02.pdf

	So this algorithm should work this way:
		* Should start with a minimal network
			* Only one connection between the input and output layer
			* OR one hidden neuron connected (upstream) to one input and connected (downstream) to one output
		* Concurrently test all network and give them a score
		* Select the best
		* Mutation: among the best, clone some of them and mutate them:
			* Change randomly some synapse weights and bias
			* Randomly remove a connection (synapse)
				* Better chance to remove it if the synapse is rarely used: if its excitation (input x weight)
				  is closed to zero most of time
			* Randomly remove a neuron
				* Better chance if its output signal is most of time closed to zero, even more chance there
				  isn't any downstream path
			* Randomly add a neuron with a unique ID (probably an auto-increment)
				* The unique ID is used for crossing over networks
			* Randomly add a connection (synapse) that doesn't violate feed-forward processing
				* Better chance if two neurons activate at the same time, it is said that they should connect
		* Crossover/breed networks: among the best, create networks that are the sum/average of 2 networks
			* Create a topology that have all neurons and connections, it's easy since all neurons have an ID,
			  and a connection can be expressed as a unique from-to combination
			* When both network have the same neuron (e.g.: a neuron with the same ID), the bias is set randomly between both value
			* When both network have the same connection, the weight of the connection is set randomly between both value
			* There is also a chance that all weight+bias of a neuron comes from only one parent, to enforce a kind of
			  specialization of each neuron?
			* Crossed networks should be preserved, this kind of innovation is not effective immediately,
			  perhaps create “Islands” for them
		* “Islands” are isolated ecosystem, only networks of the same island are put in concurrency
			* “Islands” are randomly reunited, the chance for a merge is greater when for every unmerged generation
*/

<|endoftext|>


<commit_before>if(typeof widget_image == 'undefined') {
    dynamicload('js/widget_image.js');
}

var widget_itunes_artwork = $.extend({}, widget_image, {
    widgetname: 'itunes_artwork',
    init_attr: function(elem) {
        elem.data('get',        elem.data('get')        || 'STATE');
        // data-get is an array
        if(! $.isArray(elem.data('get'))) {
            // ..like it or lump it
            elem.data('get', new Array(elem.data('get')));
        }
        // create a data-var for each field of the get-array and subscribe to it's update events
        for(var g=0; g<elem.data('get').length; g++) {
            var get = elem.data('get')[g];
            elem.data(get, get);
            readings[get] = true;
        }

        elem.data('opacity',    elem.data('opacity')    || 1);
        elem.data('size',       elem.data('size')       || 150);
        elem.data('height',     elem.data('size'));
        elem.data('width',      elem.data('size'));
        elem.data('media',      elem.data('media')      || 'music');
        elem.data('entity',     elem.data('entity')     || 'song');
        elem.data('url',        elem.data('url'));
        elem.data('refresh',    elem.data('refresh')    || 15*60);
    },
    update_value_cb : function(value) {
        if(value && value.match(/^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d$/)) {
            return '';
        }
        return value;
    },
    update: function (dev,par) {
        base = this;
        var deviceElements = this.elements.filter('div[data-device="'+dev+'"]');
        deviceElements.each(function(index) {
            var img = $(this).find('img');

            var done=0;
			var get = $(this).data('get');
            var val = new Array();
            for(var g=0; g<get.length; g++) {
                // get all readings
                val[g] = getDeviceValue($(this), get[g]);

                // count read values
                if(val[g]) {
                    done++;
                }
            }

            // fetch coverimage after all readings are read
            if(val.length == done) {
			    // delete timestamp values (workarroud for list-bug in requestFhem)
			    for(var g=0; g<get.length; g++) {
			        val[g] = base.update_value_cb(val[g]);
			    }

			    $.ajax({
                    url: "https://itunes.apple.com/search",
                    dataType: "jsonp",
                    data: {
                        term:     val.join(' '),
                        media:    $(this).data('media'),
                        entity:   $(this).data('entity'),
                    },
                    size: $(this).data('size'),
                    error: function (jqXHR, textStatus, message) {
                        console.log(message);
                    },
                    success: function (data, textStatus, jqXHR) {
                        if($.isArray(data.results) && data.results[0] && data.results[0].artworkUrl100) {
                            var artwork;
                            if(this.size <=60) {
                                artwork = data.results[0].artworkUrl60;
                            } else {
                                artwork = data.results[0].artworkUrl100;
                            }
                            if(artwork) {
			                    artwork = artwork.replace(/100x100/, this.size+'x'+this.size);
			                    console.log(artwork);
			                    img.attr('src', artwork);
			                }
			            }
                    },
                });
            }
	    });
    }
});

// https://www.apple.com/itunes/affiliates/resources/documentation/itunes-store-web-service-search-api.html<commit_msg>Update widget_itunes_artwork.js<commit_after>if(typeof widget_image == 'undefined') {
    dynamicload('js/widget_image.js');
}

var widget_itunes_artwork = $.extend({}, widget_image, {
    widgetname: 'itunes_artwork',
    init_attr: function(elem) {
        elem.data('get',        elem.data('get')        || 'STATE');
        // data-get is an array
        if(! $.isArray(elem.data('get'))) {
            // ..like it or lump it
            elem.data('get', new Array(elem.data('get')));
        }
        // create a data-var for each field of the get-array and subscribe to it's update events
        for(var g=0; g<elem.data('get').length; g++) {
            var get = elem.data('get')[g];
            elem.data(get, get);
            readings[get] = true;
            console.log(this.widgetname, 'init_attr', get);
        }

        elem.data('opacity',    elem.data('opacity')    || 1);
        elem.data('size',       elem.data('size')       || 150);
        elem.data('height',     elem.data('size'));
        elem.data('width',      elem.data('size'));
        elem.data('media',      elem.data('media')      || 'music');
        elem.data('entity',     elem.data('entity')     || 'song');
        elem.data('timeout',    elem.data('timeout')    || 3000);
        elem.data('loadingimg', elem.data('loadingimg') || 'http://vignette1.wikia.nocookie.net/knightsanddragons/images/c/c2/Peanut-butter-jelly-time.gif/revision/latest?cb=20140709170448'); // remember to change this
        elem.data('url',        elem.data('url'));
        elem.data('refresh',    elem.data('refresh')    || 15*60);

        elem.find('img').attr('src', elem.data('loadingimg'));
    },
    update_value_cb : function(value) {
        if(value && value.match(/^\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d$/)) {
            return '';
        }
        return value;
    },
    itunes: function (elem, val) {
        console.log(this.widgetname, 'itunes.start', val);
        $.ajax({
            url: "https://itunes.apple.com/search",
            dataType: "jsonp",
            data: {
                term:       val.join(' '),
                media:      elem.data('media'),
                entity:     elem.data('entity'),
            },
            base:           this,
            size:           elem.data('size'),
            img:            elem.find('img'),
            timeout:        elem.data('timeout'),
            beforeSend: function(jqXHR, settings) {
                jqXHR.url = settings.url;
            },
            error: function (jqXHR, textStatus, message) {
                console.log(this.base.widgetname, 'itunes.error', textStatus, message, jqXHR.url);
            },
            success: function (data, textStatus, jqXHR) {
                if($.isArray(data.results) && data.results[0] && data.results[0].artworkUrl100) {
                    var artwork;
                    if(this.size <=60) {
                        artwork = data.results[0].artworkUrl60;
                    } else {
                        artwork = data.results[0].artworkUrl100;
                    }
                    if(artwork) {
                        artwork = artwork.replace(/100x100/, this.size+'x'+this.size);
                        this.img.attr('src', artwork);
                        console.log(this.base.widgetname, 'itunes.artwork', artwork);
                    } else {
                        console.log(this.base.widgetname, 'itunes.artwork', '-');
                    }
                } else {
                    console.log(this.base.widgetname, 'itunes.results', '-');
                    if(val.length>1) {
                        val.pop();
                        this.base.itunes(elem, val);
                    }
                }
            },
        });
    },
    update: function (dev,par) {
        base = this;
        var deviceElements = this.elements.filter('div[data-device="'+dev+'"]');
        deviceElements.each(function(index) {
            var done=0;
			var get = $(this).data('get');
            var val = new Array();
            for(var g=0; g<get.length; g++) {
                // get all readings
                val[g] = getDeviceValue($(this), get[g]);

                // count read values
                if(val[g]) {
                    done++;
                }
            }

            // fetch coverimage after all readings are read
            if(val.length == done && ! $(this).data('updateinprogress')) {
			    // try to make it behave a little
			    $(this).data('updateinprogress', true);
			    // delete timestamp values (workarroud for list-bug in requestFhem)
			    for(var g=0; g<get.length; g++) {
			        val[g] = base.update_value_cb(val[g]);
			    }
                console.log(base.widgetname, 'update', get, val);
			    $(this).find('img').attr('src', $(this).data('loadingimg'));
			    base.itunes($(this), val);
            }
            $(this).data('updateinprogress', false);
	    });
    }
});

// https://www.apple.com/itunes/affiliates/resources/documentation/itunes-store-web-service-search-api.html
<|endoftext|>


<commit_before>'use strict';
(function(module) {
  try {
    module = angular.module('tink.fieldset');
  } catch (e) {
    module = angular.module('tink.fieldset', []);
  }
  module.directive('tinkFieldset', ['safeApply','$timeout','$compile',function (safeApply,$timeout,$compile) {
    return {
      restrict: 'A',
      scope: {
        tinkFormEditable:'=?',
        tinkFormStatus:'=?'
      },
      compile:function(template){
        return {
        post: function () { },
        pre: function (scope, element) {
        var disabledReady=[];

        var activeElement = null;

        var classToSetWhenOnHover = 'mouseOver';
        var classToSetWhenDefault = 'mouseOff';
        var classToSetWhenActive = 'mouseFocus';
        var cssUneditableClass = 'uneditable';

        var activeClass = '';
        function setClassActive(cssClass,elem){
          safeApply(scope,function(){
            $timeout(function() {
              if((activeElement === $(elem).get(0) || activeElement === null)){
                $(element).removeClass(classToSetWhenOnHover);
                $(element).removeClass(classToSetWhenDefault);
                $(element).removeClass(classToSetWhenActive);
                $(element).removeClass(cssUneditableClass);
                $(element).addClass(cssClass);
                activeClass = cssClass;
                scope.tinkFormStatus = cssClass;
              }
            }, 10);

          });
        }

        setClassActive(classToSetWhenDefault);
        scope.formFocus = 0;
        $(element).bind('mousedown',function(){
            safeApply(scope,function(){
              if(scope.formFocus === 0){
                $('body').bind('mousedown',function(e){
                //var target = $(e.target);
                  if($(element).get(0) !== $(e.target).get(0) && $(element).find($(e.target)).length === 0){
                    scope.formFocus = 0;
                     $('body').unbind('mousedown');
                     activeElement = $(element).get(0);
                     setClassActive(classToSetWhenDefault,$(element).get(0));
                  }
              });
            }
            scope.formFocus = 1;
          });
        });

        scope.$watch('tinkFormEditable',function(value){
          if(!value){
            $(template).addClass(cssUneditableClass);
            getElements().each(function(index,elem){
              safeApply(scope,function(){
                if($(elem).isolateScope()){
                  $(elem).isolateScope().isDisabled = true;
                }
                $(elem).attr('disabled','true');
                specialElementsFix($(elem),false);
              });
            });
          }else{
            $(template).removeClass(cssUneditableClass);
            getElements().each(function(index,elem){
              if(disabledReady.indexOf($(elem).get(0)) === -1){
                safeApply(scope,function(){
                  if($(elem).isolateScope()){
                    $(elem).isolateScope().isDisabled = false;
                  }
                  $(elem).removeAttr('disabled');
                  specialElementsFix($(elem),true);
                });
              }
            });
          }
        });

        function specialElementsFix(element,enable){
          if($(element).is('select')){
            elementSelectFix(element,enable);
          }
        }

        function elementSelectFix(element,enable){
          if(!enable){
            element.css('display','none');
            var model = element.attr('ng-model') || element.attr('data-ng-model');
            var input = $('<input type="text" disabled ng-model="'+model+'"/>');
            input = $compile(input)(scope.$parent);
            input.insertAfter(element);
          }else{
            element.css('display','block');
            if(element.next().is('input')){
              element.next().remove();
            }
          }
        }

        function isDisabled(elem){
          var targetEl = $(elem);
          var itIsDisabled = targetEl.attr('disabled') || targetEl.attr('data-disabled') || targetEl.attr('is-disabled') || targetEl.attr('data-is-disabled');
          if(itIsDisabled){
            return true;
          }
          return false;
        }

        function mouseOverEvent(e){
          safeApply(scope,function(){
              if(activeClass !== classToSetWhenActive && !isDisabled(e.target)){
                activeElement = $(e.target).get(0);
                setClassActive(classToSetWhenOnHover,$(e.target).get(0));
              }
          });
        }

        function mouseOutEvent(e){
          safeApply(scope,function(){
              if(activeClass !== classToSetWhenActive && !isDisabled(e.target)){
                setClassActive(classToSetWhenDefault,$(e.target).get(0));
               // activeElement = null;
              }
          });
        }

        function focusEvent(e){
          safeApply(scope,function(){
              activeElement = $(e.target).get(0);
              setClassActive(classToSetWhenActive,$(e.target).get(0));
          });
        }

        function getElements(){
          var mouseElements = element.find('[ng-model], [data-ng-model]');
          return mouseElements;
        }

        function addEventsToElements(){
          getElements().each(function(index,elem){
            scope.addEvents($(elem));
          });
        }

        scope.addEvents = function(elem){
          $(elem).focusin(elem,focusEvent);
          $(elem).mouseover(elem,mouseOverEvent);
          $(elem).mouseout(elem,mouseOutEvent);
        };
        scope.removeEvents = function(elem){
          $(elem).unbind('mouseover mouseout focusin');
        };

        scope.setClassActive = function(clss,elem){
          setClassActive(clss,elem)
        }

         addEventsToElements();
      }
    };
  }
    };
  }]);
})();<commit_msg>fix wrong variable name<commit_after>'use strict';
(function(module) {
  try {
    module = angular.module('tink.fieldset');
  } catch (e) {
    module = angular.module('tink.fieldset', []);
  }
  module.directive('tinkFieldset', ['safeApply','$timeout','$compile',function (safeApply,$timeout,$compile) {
    return {
      restrict: 'A',
      scope: {
        tinkFormEditable:'=?',
        tinkFormStatus:'=?'
      },
      compile:function(template){
        return {
        post: function () { },
        pre: function (scope, element) {
        var disabledReady=[];

        var activeElement = null;

        var classToSetWhenOnHover = 'mouseOver';
        var classToSetWhenDefault = 'mouseOff';
        var classToSetWhenActive = 'mouseFocus';
        var cssUneditableClass = 'uneditable';

        var activeClass = '';
        function setClassActive(cssClass,elem){
          safeApply(scope,function(){
            $timeout(function() {
              if((activeElement === $(elem).get(0) || elem === null)){
                $(element).removeClass(classToSetWhenOnHover);
                $(element).removeClass(classToSetWhenDefault);
                $(element).removeClass(classToSetWhenActive);
                $(element).removeClass(cssUneditableClass);
                $(element).addClass(cssClass);
                activeClass = cssClass;
                scope.tinkFormStatus = cssClass;
              }
            }, 10);

          });
        }

        setClassActive(classToSetWhenDefault);
        scope.formFocus = 0;
        $(element).bind('mousedown',function(){
            safeApply(scope,function(){
              if(scope.formFocus === 0){
                $('body').bind('mousedown',function(e){
                //var target = $(e.target);
                  if($(element).get(0) !== $(e.target).get(0) && $(element).find($(e.target)).length === 0){
                    scope.formFocus = 0;
                     $('body').unbind('mousedown');
                     activeElement = $(element).get(0);
                     setClassActive(classToSetWhenDefault,$(element).get(0));
                  }
              });
            }
            scope.formFocus = 1;
          });
        });

        scope.$watch('tinkFormEditable',function(value){
          if(!value){
            $(template).addClass(cssUneditableClass);
            getElements().each(function(index,elem){
              safeApply(scope,function(){
                if($(elem).isolateScope()){
                  $(elem).isolateScope().isDisabled = true;
                }
                $(elem).attr('disabled','true');
                specialElementsFix($(elem),false);
              });
            });
          }else{
            $(template).removeClass(cssUneditableClass);
            getElements().each(function(index,elem){
              if(disabledReady.indexOf($(elem).get(0)) === -1){
                safeApply(scope,function(){
                  if($(elem).isolateScope()){
                    $(elem).isolateScope().isDisabled = false;
                  }
                  $(elem).removeAttr('disabled');
                  specialElementsFix($(elem),true);
                });
              }
            });
          }
        });

        function specialElementsFix(element,enable){
          if($(element).is('select')){
            elementSelectFix(element,enable);
          }
        }

        function elementSelectFix(element,enable){
          if(!enable){
            element.css('display','none');
            var model = element.attr('ng-model') || element.attr('data-ng-model');
            var input = $('<input type="text" disabled ng-model="'+model+'"/>');
            input = $compile(input)(scope.$parent);
            input.insertAfter(element);
          }else{
            element.css('display','block');
            if(element.next().is('input')){
              element.next().remove();
            }
          }
        }

        function isDisabled(elem){
          var targetEl = $(elem);
          var itIsDisabled = targetEl.attr('disabled') || targetEl.attr('data-disabled') || targetEl.attr('is-disabled') || targetEl.attr('data-is-disabled');
          if(itIsDisabled){
            return true;
          }
          return false;
        }

        function mouseOverEvent(e){
          safeApply(scope,function(){
              if(activeClass !== classToSetWhenActive && !isDisabled(e.target)){
                activeElement = $(e.target).get(0);
                setClassActive(classToSetWhenOnHover,$(e.target).get(0));
              }
          });
        }

        function mouseOutEvent(e){
          safeApply(scope,function(){
              if(activeClass !== classToSetWhenActive && !isDisabled(e.target)){
                setClassActive(classToSetWhenDefault,$(e.target).get(0));
               // activeElement = null;
              }
          });
        }

        function focusEvent(e){
          safeApply(scope,function(){
              activeElement = $(e.target).get(0);
              setClassActive(classToSetWhenActive,$(e.target).get(0));
          });
        }

        function getElements(){
          var mouseElements = element.find('[ng-model], [data-ng-model]');
          return mouseElements;
        }

        function addEventsToElements(){
          getElements().each(function(index,elem){
            scope.addEvents($(elem));
          });
        }

        scope.addEvents = function(elem){
          $(elem).focusin(elem,focusEvent);
          $(elem).mouseover(elem,mouseOverEvent);
          $(elem).mouseout(elem,mouseOutEvent);
        };
        scope.removeEvents = function(elem){
          $(elem).unbind('mouseover mouseout focusin');
        };

        scope.setClassActive = function(clss,elem){
          setClassActive(clss,elem)
        }

         addEventsToElements();
      }
    };
  }
    };
  }]);
})();<|endoftext|>