<commit_before>'use strict';

requirejs.config({
  baseUrl: '/javascripts/',
  enforceDefine: true,
  paths: {
    jquery: '/javascripts/jquery'
  }
});

define(['jquery', 'appnet', 'friends', 'user', 'jquery.caret'],
  function($, appnet, friends, user) {
  var body = $('body');
  var url = body.data('url');
  var tabs = body.find('ol.tabs');
  var messages = body.find('ol.messages');
  var write = body.find('.write form');
  var userInfo = body.find('.user-info');
  var overlay = body.find('#overlay');
  var charLimit = body.find('.counter');
  var suggestions = body.find('ol.suggestions');
  var notifications = body.find('#notifications-preview');
  var notificationIcon = body.find('#notifications');
  var win = $(window);
  var doc = $(document);
  var csrf = write.find('input[name="_csrf"]').val();
  var postLoad = false;
  var notificationsDisplay = false;
  var currentMessage;

  var CHAR_MAX = parseInt(body.data('charlimit'), 10);

  var
    TAB_KEYCODE = 9,
    RETURN_KEYCODE = 13,
    ESCAPE_KEYCODE = 27,
    SPACE_KEYCODE = 32,
    F_KEYCODE = 70,
    J_KEYCODE = 74,
    K_KEYCODE = 75,
    P_KEYCODE = 80,
    Q_KEYCODE = 81,
    R_KEYCODE = 82,
    S_KEYCODE = 83,
    T_KEYCODE = 84,
    U_KEYCODE = 85;

  var MESSAGE_LIMIT = 99;

  var resetNotificationDisplay = function() {
    notificationIcon
      .removeClass('on')
      .text(0);
    document.title = 'NoodleApp';
    appnet.resetUnread();
  };

  var resetTab = function(self, callback) {
    self.siblings().removeClass('selected');
    self.addClass('selected');
    callback();
  };

  var checkUrl = function() {
    var hash = document.location.hash;

    if (hash.indexOf('/post/') > -1) {
      var postArray = hash.split('/post/')[1].split('/');
      var postId = postArray[0];
      appnet.showPost(postId);
      body.addClass('fixed');
    } else if (hash.indexOf('/tagged/') > -1) {
      appnet.showTagged(hash.split('/tagged/')[1]);
      body.addClass('fixed');
    } else if (hash.length < 3 && body.hasClass('fixed')) {
       closeOverlay();
    }
  };

  // Length of actual text that will be posted to app.net
  var getEffectiveLength = function(text) {
    // Same as in markdown-to-entities, but global
    var markdownLinkRegex = /\[([^\]]+)\]\((\S+(?=\)))\)/g;
    return text.replace(markdownLinkRegex, '$1').length;
  };

  var checkCharLimit = function(text) {
    var textLength = getEffectiveLength(text);
    write.find('button').toggleClass('disabled', textLength === 0);
    if (textLength > CHAR_MAX) {
      charLimit.addClass('over');
      charLimit.text('- ' + (textLength - CHAR_MAX));
    } else {
      charLimit.removeClass('over');
      charLimit.text(CHAR_MAX - textLength);
    }
  };

  var updateFeed = function(self, callback) {
    messages.empty();
    resetTab(self, function() {
      callback();
    });
  };

  var closeOverlay = function() {
    window.history.pushState('', '', document.location.href.split('#')[0]);
    overlay.find('.inner-overlay').html('');
    overlay.find('textarea').val('');
    overlay.slideUp(function() {
      body.removeClass('fixed');
    });
  };

  var saveSettings = function(self) {
    var directedFeed = false;
    var mediaOn = false;
    var charLimit = false;

    if (self.hasClass('on')) {
      self.removeClass('on');
    } else {
      self.addClass('on');
    }

    if (overlay.find('#directed-feed').hasClass('on')) {
      directedFeed = true;
    }

    if (overlay.find('#media-on').hasClass('on')) {
      mediaOn = true;
    }

    if (overlay.find('#charlimit').hasClass('on')) {
      charLimit = true;
    }

    user.saveSettings(directedFeed, mediaOn, charLimit, write.find('input[name="_csrf"]').val());
  };

  /* Feed functionality */
  tabs.on('click', function(ev) {
    var self = $(ev.target);

    switch (true) {
      case self.hasClass('global-feed'):
        updateFeed(self, function() {
          appnet.getGlobalFeed();
        });
        break;

      case self.hasClass('user-posts'):
        updateFeed(self, function() {
          appnet.getUserPosts();
        });
        break;

      case self.hasClass('user-mentions'):
        updateFeed(self, function() {
          appnet.getUserMentions();
        });
        break;

      case self.hasClass('user-interactions'):
        updateFeed(self, function() {
          appnet.getUserInteractions();
        });
        break;

      case self.hasClass('user-starred'):
        updateFeed(self, function() {
          appnet.getUserStarred();
        });
        break;

      default:
        updateFeed(self, function() {
          appnet.getMyFeed();
        });
        break;
    }
  });

  /* Automatic feed loader */
  tabs.find('.selected').click();

  /* Message functionality */
  messages.on('click', function(ev) {
    var self = $(ev.target);

    switch (true) {
      case self.hasClass('reply'):
        self.closest('.message-item').find('time').click();
        break;

      case self.hasClass('quote'):
        var textarea = self.closest('.dashboard-content').find('textarea');
        textarea.focus();
        write.find('.form-action-wrapper').slideDown('fast');
        textarea.val('"' + self.closest('.message-item').data('original') + '"');
        break;

      case self.hasClass('delete'):
        appnet.deleteMessage(self.closest('.message-item').data('id'), csrf);
        self.closest('li.message-item').fadeOut();
        break;

      case self.hasClass('thread'):
        appnet.showThread(self.closest('.message-item').data('id'));
        body.addClass('fixed');
        break;

      case self.is('#paginated'):
        appnet.getOlderPosts(self.prev().data('id'));
        break;
    }
  });

  var windowHasScrolled = false;
  var scollCheck = setInterval(function () {
    if (windowHasScrolled) {
      var nearBottom = win.scrollTop() > doc.height() - win.height() - 200;
      if (nearBottom) {
        $('#paginated').click();
      }
    }
    windowHasScrolled = false;
  }, 100);

  win.on('scroll', function () {
    windowHasScrolled = true;
  });

  // Overlay functionality
  overlay.on('click', function(ev) {
    var self = $(ev.target);

    switch (true) {
      case self.hasClass('reply'):
        var messageItem = self.closest('.message-item');
        var mentions = (messageItem.data('mentions') !== '') ? messageItem.data('mentions') + ' ' : '';

        overlay.find('textarea')
          .focus()
          .val('@' + messageItem.data('username') + ' ' + mentions);

        if (parseInt(messageItem.data('replyto'), 10) > 0) {
          overlay.find('.reply_to').val(messageItem.data('replyto'));

        } else {
         overlay.find('.reply_to').val(messageItem.data('id'));
        }
        break;

      case self.hasClass('thread'):
        appnet.showThread(self.closest('.message-item').data('id'));
        body.addClass('fixed');
        break;

      case self.hasClass('close'):
        closeOverlay();
        break;

      case self.hasClass('quote'):
        var textarea = overlay.find('textarea');
        textarea.focus();
        overlay.find('.form-action-wrapper').slideDown('fast');
        textarea.val('"' + self.closest('.message-item').data('original') + '"');
        break;
    }
  });

  // General activity for all shared functionality
  body.on('click', function(ev) {
    var self = $(ev.target);

    if (!self.hasClass('writeable')) {
      suggestions.empty();
    }

    if (notificationsDisplay &&
      !(self.is('#notifications-preview') ||
      self.is('#notifications') ||
      self.closest('#notifications-preview').length)) {

      notifications.slideUp();
      notificationsDisplay = false;
    }

    switch (true) {
      case self.hasClass('star'):
        if (self.hasClass('on')) {
          self.removeClass('on');
          self.find('span').text('Star');
          appnet.unstarMessage(self.closest('.message-item').data('id'), csrf);
        } else {
          self.addClass('on');
          self.find('span').text('Unstar');
          appnet.starMessage(self.closest('.message-item').data('id'), csrf);
        }
        break;

      case self.hasClass('repost'):
        if (self.hasClass('on')) {
          self.removeClass('on');
          self.find('span').text('Repost');
          appnet.unrepostMessage(self.closest('.message-item').data('id'), csrf);
        } else {
          self.addClass('on');
          self.find('span').text('Unrepost');
          appnet.repostMessage(self.closest('.message-item').data('id'), csrf);
        }
        break;

      case self.is('time'):
        var textarea = overlay.find('textarea');
        var closest = self.closest('.message-item');

        document.location.hash = '/post/' + closest.data('id') +
          '/' + closest.data('username');
        break;

      case self.is('#notifications'):
        var self = $(this);
        resetNotificationDisplay();

        if (!notificationsDisplay) {
          notifications.slideDown();
          notificationsDisplay = true;

        } else {
          notifications.slideUp();
          notificationsDisplay = false;
        }
        break;

      case self.hasClass('tags'):
        ev.preventDefault();
        document.location.hash = self.attr('href');
        break;

      case self.is('#settings-link'):
        user.getSettings();
        break;

      case self.is('#charlimit'):
      case self.is('#media-on'):
      case self.is('#directed-feed'):
        saveSettings(self);
        break;

      case self.parent().is('#unread-messages'):
        appnet.clearUnread(self.parent());
        resetNotificationDisplay();
        if (messages.find('> li').length >= 20 && messages.find('#paginated').length === 0) {
          messages.append('
View Older
');
        }
        break;
    }
  });

  var keyCodeToClassName = {};
  keyCodeToClassName[T_KEYCODE] = '.thread';
  keyCodeToClassName[S_KEYCODE] = '.star';
  keyCodeToClassName[R_KEYCODE] = '.reply';
  keyCodeToClassName[P_KEYCODE] = '.repost';
  keyCodeToClassName[Q_KEYCODE] = '.quote';

  body.on('keydown', function(ev) {
    var self = $(ev.target);

    if (ev.keyCode === ESCAPE_KEYCODE && body.hasClass('fixed')) {
      closeOverlay();
    }

    switch (true) {
      case self.hasClass('submittable'):
        if (ev.keyCode === TAB_KEYCODE || ev.keyCode === SPACE_KEYCODE) {
          // Pressing TAB/space autocompletes to the first user listed.
          var userLi = suggestions.find('li:first');
          if (userLi.length) {
            friends.setUser(userLi, self, self.getCursorPosition());
            suggestions.empty();
            return false;
          }

        } else if (ev.keyCode === RETURN_KEYCODE && (ev.ctrlKey || ev.metaKey)) {
          self.closest('form').submit();
        }
    }

    if (!self.is('textarea')) {
      if (ev.keyCode === K_KEYCODE || ev.keyCode === J_KEYCODE) {
        currentMessage = body.find('.message-item.hover');
        if (!currentMessage.length) {
          currentMessage = body.find('.message-item:first');
          currentMessage.addClass('hover');
          return;
        }

        var next;
        if (ev.keyCode === K_KEYCODE) {
          next = currentMessage.prev('.message-item');
        } else {
          next = currentMessage.next('.message-item');
        }

        if (next.length) {
          currentMessage.removeClass('hover');
          next.addClass('hover');
          $(window).scrollTop(next.position().top - 40);
          currentMessage = next;
        }
        ev.preventDefault();
      }

      if (currentMessage && keyCodeToClassName[ev.keyCode]) {
        currentMessage.find(keyCodeToClassName[ev.keyCode]).click();
        ev.preventDefault();
      }
    }
  });

  /* User functionality */
  userInfo.on('click', function(ev) {
    var self = $(ev.target);

    switch (true) {
      case self.hasClass('followers'):
        appnet.showFollowers();
        body.addClass('fixed');
        break;

      case self.hasClass('following'):
        appnet.showFollowing();
        body.addClass('fixed');
        break;

      case self.hasClass('follow'):
        if (self.hasClass('on')) {
          self.removeClass('on');
          self.text('Follow');
          appnet.unfollow(self.parent().data('userid'), self.parent().data('username'), csrf);

        } else {
          self.addClass('on');
          self.text('Unfollow');
          appnet.follow(self.parent().data('userid'), self.parent().data('username'), csrf);
        }
        break;

      case self.hasClass('mute'):
        if (self.hasClass('on')) {
          self.removeClass('on');
          self.text('Mute');
          appnet.unmute(self.parent().data('userid'), self.parent().data('username'), csrf);

        } else {
          self.addClass('on');
          self.text('Unmute');
          appnet.mute(self.parent().data('userid'), self.parent().data('username'), csrf);
        }
        break;
    }
  });

  checkUrl();

  $(window).bind('hashchange', function() {
    checkUrl();
  });

  /* Notifications */
  notifications.on('click', '.notification-item', function(ev) {
    ev.preventDefault();
    var self = $(this);

    notifications.slideUp();
    appnet.showPost(self.data('postid'));
    body.addClass('fixed');
  });

  /* Write functionality */

  checkCharLimit(write.find('textarea').val());

  write.find('textarea').focus(function() {
    var self = $(this);
    self.addClass('on');
    charLimit.addClass('on');
    checkCharLimit(self.val());
    self.closest('form').find('.form-action-wrapper').slideDown('fast');
  });

  write.find('textarea').blur(function() {
    var self = $(this);

    charLimit.removeClass('on');

    if (self.val().replace(/\s/, '').length < 1) {
      self.removeClass('on');
      self.closest('form').find('.form-action-wrapper').slideUp('fast');
    }
  });

  // Clear the reply_to id if this is empty
  write.find('textarea').keyup(function(evt) {
    var self = $(this);
    checkCharLimit(self.val());
    friends.getBFFs(self, self.getCursorPosition());
    if (self.val().trim().length === 0) {
      write.find('.reply_to').val('');
    }

  })

  write.submit(function(ev) {
    ev.preventDefault();

    var self = $(this);
    appnet.postMessage(self);
    url = '/my/feed';
    self.find('textarea').val('');
    charLimit.text(CHAR_MAX);
    write.find('.reply_to').val('');
    return false;
  });

  suggestions.on('click', 'li', function() {
    var self = $(this);
    var textarea = self.closest('form').find('textarea');
    friends.setUser(self, textarea, textarea.getCursorPosition());
  });
});
<commit_msg>Ignore j/k/action button key shortcuts when special keys are down.<commit_after>'use strict';

requirejs.config({
  baseUrl: '/javascripts/',
  enforceDefine: true,
  paths: {
    jquery: '/javascripts/jquery'
  }
});

define(['jquery', 'appnet', 'friends', 'user', 'jquery.caret'],
  function($, appnet, friends, user) {
  var body = $('body');
  var url = body.data('url');
  var tabs = body.find('ol.tabs');
  var messages = body.find('ol.messages');
  var write = body.find('.write form');
  var userInfo = body.find('.user-info');
  var overlay = body.find('#overlay');
  var charLimit = body.find('.counter');
  var suggestions = body.find('ol.suggestions');
  var notifications = body.find('#notifications-preview');
  var notificationIcon = body.find('#notifications');
  var win = $(window);
  var doc = $(document);
  var csrf = write.find('input[name="_csrf"]').val();
  var postLoad = false;
  var notificationsDisplay = false;
  var currentMessage;

  var CHAR_MAX = parseInt(body.data('charlimit'), 10);

  var
    TAB_KEYCODE = 9,
    RETURN_KEYCODE = 13,
    ESCAPE_KEYCODE = 27,
    SPACE_KEYCODE = 32,
    F_KEYCODE = 70,
    J_KEYCODE = 74,
    K_KEYCODE = 75,
    P_KEYCODE = 80,
    Q_KEYCODE = 81,
    R_KEYCODE = 82,
    S_KEYCODE = 83,
    T_KEYCODE = 84,
    U_KEYCODE = 85;

  var MESSAGE_LIMIT = 99;

  var resetNotificationDisplay = function() {
    notificationIcon
      .removeClass('on')
      .text(0);
    document.title = 'NoodleApp';
    appnet.resetUnread();
  };

  var resetTab = function(self, callback) {
    self.siblings().removeClass('selected');
    self.addClass('selected');
    callback();
  };

  var checkUrl = function() {
    var hash = document.location.hash;

    if (hash.indexOf('/post/') > -1) {
      var postArray = hash.split('/post/')[1].split('/');
      var postId = postArray[0];
      appnet.showPost(postId);
      body.addClass('fixed');
    } else if (hash.indexOf('/tagged/') > -1) {
      appnet.showTagged(hash.split('/tagged/')[1]);
      body.addClass('fixed');
    } else if (hash.length < 3 && body.hasClass('fixed')) {
       closeOverlay();
    }
  };

  // Length of actual text that will be posted to app.net
  var getEffectiveLength = function(text) {
    // Same as in markdown-to-entities, but global
    var markdownLinkRegex = /\[([^\]]+)\]\((\S+(?=\)))\)/g;
    return text.replace(markdownLinkRegex, '$1').length;
  };

  var checkCharLimit = function(text) {
    var textLength = getEffectiveLength(text);
    write.find('button').toggleClass('disabled', textLength === 0);
    if (textLength > CHAR_MAX) {
      charLimit.addClass('over');
      charLimit.text('- ' + (textLength - CHAR_MAX));
    } else {
      charLimit.removeClass('over');
      charLimit.text(CHAR_MAX - textLength);
    }
  };

  var updateFeed = function(self, callback) {
    messages.empty();
    resetTab(self, function() {
      callback();
    });
  };

  var closeOverlay = function() {
    window.history.pushState('', '', document.location.href.split('#')[0]);
    overlay.find('.inner-overlay').html('');
    overlay.find('textarea').val('');
    overlay.slideUp(function() {
      body.removeClass('fixed');
    });
  };

  var saveSettings = function(self) {
    var directedFeed = false;
    var mediaOn = false;
    var charLimit = false;

    if (self.hasClass('on')) {
      self.removeClass('on');
    } else {
      self.addClass('on');
    }

    if (overlay.find('#directed-feed').hasClass('on')) {
      directedFeed = true;
    }

    if (overlay.find('#media-on').hasClass('on')) {
      mediaOn = true;
    }

    if (overlay.find('#charlimit').hasClass('on')) {
      charLimit = true;
    }

    user.saveSettings(directedFeed, mediaOn, charLimit, write.find('input[name="_csrf"]').val());
  };

  /* Feed functionality */
  tabs.on('click', function(ev) {
    var self = $(ev.target);

    switch (true) {
      case self.hasClass('global-feed'):
        updateFeed(self, function() {
          appnet.getGlobalFeed();
        });
        break;

      case self.hasClass('user-posts'):
        updateFeed(self, function() {
          appnet.getUserPosts();
        });
        break;

      case self.hasClass('user-mentions'):
        updateFeed(self, function() {
          appnet.getUserMentions();
        });
        break;

      case self.hasClass('user-interactions'):
        updateFeed(self, function() {
          appnet.getUserInteractions();
        });
        break;

      case self.hasClass('user-starred'):
        updateFeed(self, function() {
          appnet.getUserStarred();
        });
        break;

      default:
        updateFeed(self, function() {
          appnet.getMyFeed();
        });
        break;
    }
  });

  /* Automatic feed loader */
  tabs.find('.selected').click();

  /* Message functionality */
  messages.on('click', function(ev) {
    var self = $(ev.target);

    switch (true) {
      case self.hasClass('reply'):
        self.closest('.message-item').find('time').click();
        break;

      case self.hasClass('quote'):
        var textarea = self.closest('.dashboard-content').find('textarea');
        textarea.focus();
        write.find('.form-action-wrapper').slideDown('fast');
        textarea.val('"' + self.closest('.message-item').data('original') + '"');
        break;

      case self.hasClass('delete'):
        appnet.deleteMessage(self.closest('.message-item').data('id'), csrf);
        self.closest('li.message-item').fadeOut();
        break;

      case self.hasClass('thread'):
        appnet.showThread(self.closest('.message-item').data('id'));
        body.addClass('fixed');
        break;

      case self.is('#paginated'):
        appnet.getOlderPosts(self.prev().data('id'));
        break;
    }
  });

  var windowHasScrolled = false;
  var scollCheck = setInterval(function () {
    if (windowHasScrolled) {
      var nearBottom = win.scrollTop() > doc.height() - win.height() - 200;
      if (nearBottom) {
        $('#paginated').click();
      }
    }
    windowHasScrolled = false;
  }, 100);

  win.on('scroll', function () {
    windowHasScrolled = true;
  });

  // Overlay functionality
  overlay.on('click', function(ev) {
    var self = $(ev.target);

    switch (true) {
      case self.hasClass('reply'):
        var messageItem = self.closest('.message-item');
        var mentions = (messageItem.data('mentions') !== '') ? messageItem.data('mentions') + ' ' : '';

        overlay.find('textarea')
          .focus()
          .val('@' + messageItem.data('username') + ' ' + mentions);

        if (parseInt(messageItem.data('replyto'), 10) > 0) {
          overlay.find('.reply_to').val(messageItem.data('replyto'));

        } else {
         overlay.find('.reply_to').val(messageItem.data('id'));
        }
        break;

      case self.hasClass('thread'):
        appnet.showThread(self.closest('.message-item').data('id'));
        body.addClass('fixed');
        break;

      case self.hasClass('close'):
        closeOverlay();
        break;

      case self.hasClass('quote'):
        var textarea = overlay.find('textarea');
        textarea.focus();
        overlay.find('.form-action-wrapper').slideDown('fast');
        textarea.val('"' + self.closest('.message-item').data('original') + '"');
        break;
    }
  });

  // General activity for all shared functionality
  body.on('click', function(ev) {
    var self = $(ev.target);

    if (!self.hasClass('writeable')) {
      suggestions.empty();
    }

    if (notificationsDisplay &&
      !(self.is('#notifications-preview') ||
      self.is('#notifications') ||
      self.closest('#notifications-preview').length)) {

      notifications.slideUp();
      notificationsDisplay = false;
    }

    switch (true) {
      case self.hasClass('star'):
        if (self.hasClass('on')) {
          self.removeClass('on');
          self.find('span').text('Star');
          appnet.unstarMessage(self.closest('.message-item').data('id'), csrf);
        } else {
          self.addClass('on');
          self.find('span').text('Unstar');
          appnet.starMessage(self.closest('.message-item').data('id'), csrf);
        }
        break;

      case self.hasClass('repost'):
        if (self.hasClass('on')) {
          self.removeClass('on');
          self.find('span').text('Repost');
          appnet.unrepostMessage(self.closest('.message-item').data('id'), csrf);
        } else {
          self.addClass('on');
          self.find('span').text('Unrepost');
          appnet.repostMessage(self.closest('.message-item').data('id'), csrf);
        }
        break;

      case self.is('time'):
        var textarea = overlay.find('textarea');
        var closest = self.closest('.message-item');

        document.location.hash = '/post/' + closest.data('id') +
          '/' + closest.data('username');
        break;

      case self.is('#notifications'):
        var self = $(this);
        resetNotificationDisplay();

        if (!notificationsDisplay) {
          notifications.slideDown();
          notificationsDisplay = true;

        } else {
          notifications.slideUp();
          notificationsDisplay = false;
        }
        break;

      case self.hasClass('tags'):
        ev.preventDefault();
        document.location.hash = self.attr('href');
        break;

      case self.is('#settings-link'):
        user.getSettings();
        break;

      case self.is('#charlimit'):
      case self.is('#media-on'):
      case self.is('#directed-feed'):
        saveSettings(self);
        break;

      case self.parent().is('#unread-messages'):
        appnet.clearUnread(self.parent());
        resetNotificationDisplay();
        if (messages.find('> li').length >= 20 && messages.find('#paginated').length === 0) {
          messages.append('
View Older
');
        }
        break;
    }
  });

  var keyCodeToClassName = {};
  keyCodeToClassName[T_KEYCODE] = '.thread';
  keyCodeToClassName[S_KEYCODE] = '.star';
  keyCodeToClassName[R_KEYCODE] = '.reply';
  keyCodeToClassName[P_KEYCODE] = '.repost';
  keyCodeToClassName[Q_KEYCODE] = '.quote';

  body.on('keydown', function(ev) {
    var self = $(ev.target);

    if (ev.keyCode === ESCAPE_KEYCODE && body.hasClass('fixed')) {
      closeOverlay();
    }

    switch (true) {
      case self.hasClass('submittable'):
        if (ev.keyCode === TAB_KEYCODE || ev.keyCode === SPACE_KEYCODE) {
          // Pressing TAB/space autocompletes to the first user listed.
          var userLi = suggestions.find('li:first');
          if (userLi.length) {
            friends.setUser(userLi, self, self.getCursorPosition());
            suggestions.empty();
            return false;
          }

        } else if (ev.keyCode === RETURN_KEYCODE && (ev.ctrlKey || ev.metaKey)) {
          self.closest('form').submit();
        }
    }

    if (!(self.is('textarea') || ev.ctrlKey || ev.shiftKey || ev.metaKey)) {
      if (ev.keyCode === K_KEYCODE || ev.keyCode === J_KEYCODE) {
        currentMessage = body.find('.message-item.hover');
        if (!currentMessage.length) {
          currentMessage = body.find('.message-item:first');
          currentMessage.addClass('hover');
          return;
        }

        var next;
        if (ev.keyCode === K_KEYCODE) {
          next = currentMessage.prev('.message-item');
        } else {
          next = currentMessage.next('.message-item');
        }

        if (next.length) {
          currentMessage.removeClass('hover');
          next.addClass('hover');
          $(window).scrollTop(next.position().top - 40);
          currentMessage = next;
        }
        ev.preventDefault();
      }

      if (currentMessage && keyCodeToClassName[ev.keyCode]) {
        currentMessage.find(keyCodeToClassName[ev.keyCode]).click();
        ev.preventDefault();
      }
    }
  });

  /* User functionality */
  userInfo.on('click', function(ev) {
    var self = $(ev.target);

    switch (true) {
      case self.hasClass('followers'):
        appnet.showFollowers();
        body.addClass('fixed');
        break;

      case self.hasClass('following'):
        appnet.showFollowing();
        body.addClass('fixed');
        break;

      case self.hasClass('follow'):
        if (self.hasClass('on')) {
          self.removeClass('on');
          self.text('Follow');
          appnet.unfollow(self.parent().data('userid'), self.parent().data('username'), csrf);

        } else {
          self.addClass('on');
          self.text('Unfollow');
          appnet.follow(self.parent().data('userid'), self.parent().data('username'), csrf);
        }
        break;

      case self.hasClass('mute'):
        if (self.hasClass('on')) {
          self.removeClass('on');
          self.text('Mute');
          appnet.unmute(self.parent().data('userid'), self.parent().data('username'), csrf);

        } else {
          self.addClass('on');
          self.text('Unmute');
          appnet.mute(self.parent().data('userid'), self.parent().data('username'), csrf);
        }
        break;
    }
  });

  checkUrl();

  $(window).bind('hashchange', function() {
    checkUrl();
  });

  /* Notifications */
  notifications.on('click', '.notification-item', function(ev) {
    ev.preventDefault();
    var self = $(this);

    notifications.slideUp();
    appnet.showPost(self.data('postid'));
    body.addClass('fixed');
  });

  /* Write functionality */

  checkCharLimit(write.find('textarea').val());

  write.find('textarea').focus(function() {
    var self = $(this);
    self.addClass('on');
    charLimit.addClass('on');
    checkCharLimit(self.val());
    self.closest('form').find('.form-action-wrapper').slideDown('fast');
  });

  write.find('textarea').blur(function() {
    var self = $(this);

    charLimit.removeClass('on');

    if (self.val().replace(/\s/, '').length < 1) {
      self.removeClass('on');
      self.closest('form').find('.form-action-wrapper').slideUp('fast');
    }
  });

  // Clear the reply_to id if this is empty
  write.find('textarea').keyup(function(evt) {
    var self = $(this);
    checkCharLimit(self.val());
    friends.getBFFs(self, self.getCursorPosition());
    if (self.val().trim().length === 0) {
      write.find('.reply_to').val('');
    }

  })

  write.submit(function(ev) {
    ev.preventDefault();

    var self = $(this);
    appnet.postMessage(self);
    url = '/my/feed';
    self.find('textarea').val('');
    charLimit.text(CHAR_MAX);
    write.find('.reply_to').val('');
    return false;
  });

  suggestions.on('click', 'li', function() {
    var self = $(this);
    var textarea = self.closest('form').find('textarea');
    friends.setUser(self, textarea, textarea.getCursorPosition());
  });
});
<|endoftext|>


<commit_before>var Cimpler      = require('../lib/cimpler'),
    util         = require('util'),
    fs           = require('fs'),
    assert       = require('assert');
    expect       = require("./expect"),
    childProcess = require('child_process'),
    testRepoSource   = __dirname + "/../fixtures/repo/",
    buildLogsPath    = "/tmp/cimpler-test-logs/";

// Current branch pointers in our test repo.
var testBranch = "aa6b0aa64229caee1b07500334a64de9e1ffcddd",
    master     = "ff47c0e58eef626f912c7e5d80d67d8796f65003",
    masterParent = "aac5cd96ddd3173678e3666d677699ea6adce875";

describe("git-build plugin", function() {
   var testRepoDirs = [tempDir(), tempDir()];

   it("should merge master into the branch under testing", function(done) {
      var concurrency = concurrencyChecker(1);
      var cimpler = new Cimpler({
         plugins: {
            "git-build": {
               repoPath: testRepoDirs[0],
               // Pass if test_branch is the build branch
               cmd: "[[ $BUILD_BRANCH == 'test-branch' ]]",
               logs: {
                  path: buildLogsPath,
                  url:  "http://www.example.com/ci-builds/"
               },
            }
         },
      });

      var check = expect(6, function() {
         cimpler.shutdown();
         done();
      });

      var expectedBuildCommits = [ testBranch, master ];
      cimpler.on('buildStarted', function(build) {
         concurrency(1);
         // git-build is supposed to lookup the sha for us.
         assert.equal(build.sha, expectedBuildCommits.shift());

         var cmd = "git rev-list --parents -n 1 HEAD";
         exec(cmd, testRepoDirs[0], assertParents);
         check();
      });

      cimpler.on('buildFinished', function(build) {
         concurrency(-1);
      });

      var expectedParents = [
         [ testBranch, master ],
         [ masterParent ]
      ];
      function assertParents(stdout) {
         var parents = stdout.split(' ');
         // drop the first hash which == HEAD
         parents.shift();
         assert.deepEqual(parents, expectedParents.shift());
         check();
      }

      var expectedStatuses = ['success', 'failure'];
      cimpler.on('buildFinished', function(build) {
         assert.equal(build.status, expectedStatuses.shift());
         check();
      });

      cimpler.addBuild({
         repo: "doesn't matter",
         branch: "test-branch"
      });
      cimpler.addBuild({
         repo: "doesn't matter",
         branch: "master"
      });
   });

   it("Create as many workers as there are repoPaths", function(done) {
      var started = 0, finished = 0;
      var cimpler = new Cimpler({
         plugins: {
            "git-build": {
               repoPath: testRepoDirs,
               // Pass if test_branch is the build branch
               cmd: "sleep 1 && [[ $BUILD_BRANCH == 'test-branch' ]]",
               logs: {
                  path: buildLogsPath,
                  url:  "http://www.example.com/ci-builds/"
               },
            }
         },
      });

      var check = expect(4, function() {
         cimpler.shutdown();
         done();
      });

      var expectedBuildCommits = {
         A: testBranch,
         B: master
      };
      cimpler.on('buildStarted', function(build) {
         assert.equal(build.sha, expectedBuildCommits[build.letter]);
         check();
         started++;
         // We should be running these in paralell
         assert.ok(finished == 0, "Both builds should start before either finish");
      });

      var expectedStatuses = {
         A: 'success',
         B: 'failure'
      };
      cimpler.on('buildFinished', function(build) {
         assert.equal(build.status, expectedStatuses[build.letter]);
         check();
         finished++;
         // We should be running these in paralell
         assert.ok(started == 2, "One build finished before both has started");
      });

      cimpler.addBuild({
         letter: 'A',
         repo: "doesn't matter",
         branch: "test-branch"
      });
      cimpler.addBuild({
         letter: 'B',
         repo: "doesn't matter",
         branch: "master"
      });
   });

   function exec(cmd, dir, callback, expectFailure) {
      var execOptions = {
         cwd: dir
      };
      childProcess.exec(cmd, execOptions, function(err, stdout, stderr) {
         if (!expectFailure != !err) {
            var msg = err ? "Command failed: " + cmd :
                            "Command was supposed to fail (but didn't): " + cmd;
            util.error(msg);
            console.log(stdout.toString());
            console.log(stderr.toString());
            process.exit(1);
         }
         callback(stdout.toString().trim());
      });
   }

   /**
    * Because Git doesn't allow adding any files or dirs named .git
    * we can't add the test repo's .git dir directly to the main repo.
    * We must resort to dynamically creating and destroying a
    * "symlink-like" file that points git to a different folder for
    * the actual repo dir.
    *
    * Actual repo dir:        fixtures/repo/git
    * "symlink" to repo dir:  fixtures/repo/.git
    */
   before(function(done) {
      // Add a 'git-style' symlink
      fs.writeFileSync(testRepoSource + '.git', "gitdir: ./git");
      // Ignore the "Already exists" errors
      try { fs.mkdirSync(buildLogsPath) } catch (err) {}
      // Clone the test repo into a temp dir.
      var cmd = testRepoDirs.map(function (dir) {
         return "git clone " + testRepoSource + " " + dir;
      }).join(" && ");
      childProcess.exec(cmd, {}, done);
   });

   after(function() {
      fs.unlink(testRepoSource + '.git');
   });
});

function concurrencyChecker(max) {
   var levels = 0;
   return function (inc) {
      levels += inc;
      if (levels > max)
         assert.fail("Concurrency should not be greater than " + max);
   };
}

function tempDir() {
   return "/tmp/cimpler-test-" +
            Math.floor(Math.random() * 999999);
}
<commit_msg>Tests: Use [ instead of [[<commit_after>var Cimpler      = require('../lib/cimpler'),
    util         = require('util'),
    fs           = require('fs'),
    assert       = require('assert');
    expect       = require("./expect"),
    childProcess = require('child_process'),
    testRepoSource   = __dirname + "/../fixtures/repo/",
    buildLogsPath    = "/tmp/cimpler-test-logs/";

// Current branch pointers in our test repo.
var testBranch = "aa6b0aa64229caee1b07500334a64de9e1ffcddd",
    master     = "ff47c0e58eef626f912c7e5d80d67d8796f65003",
    masterParent = "aac5cd96ddd3173678e3666d677699ea6adce875";

describe("git-build plugin", function() {
   var testRepoDirs = [tempDir(), tempDir()];

   it("should merge master into the branch under testing", function(done) {
      var concurrency = concurrencyChecker(1);
      var cimpler = new Cimpler({
         plugins: {
            "git-build": {
               repoPath: testRepoDirs[0],
               // Pass if test_branch is the build branch
               cmd: "[ \"$BUILD_BRANCH\" = 'test-branch' ]",
               logs: {
                  path: buildLogsPath,
                  url:  "http://www.example.com/ci-builds/"
               },
            }
         },
      });

      var check = expect(6, function() {
         cimpler.shutdown();
         done();
      });

      var expectedBuildCommits = [ testBranch, master ];
      cimpler.on('buildStarted', function(build) {
         concurrency(1);
         // git-build is supposed to lookup the sha for us.
         assert.equal(build.sha, expectedBuildCommits.shift());

         var cmd = "git rev-list --parents -n 1 HEAD";
         exec(cmd, testRepoDirs[0], assertParents);
         check();
      });

      cimpler.on('buildFinished', function(build) {
         concurrency(-1);
      });

      var expectedParents = [
         [ testBranch, master ],
         [ masterParent ]
      ];
      function assertParents(stdout) {
         var parents = stdout.split(' ');
         // drop the first hash which == HEAD
         parents.shift();
         assert.deepEqual(parents, expectedParents.shift());
         check();
      }

      var expectedStatuses = ['success', 'failure'];
      cimpler.on('buildFinished', function(build) {
         assert.equal(build.status, expectedStatuses.shift());
         check();
      });

      cimpler.addBuild({
         repo: "doesn't matter",
         branch: "test-branch"
      });
      cimpler.addBuild({
         repo: "doesn't matter",
         branch: "master"
      });
   });

   it("Create as many workers as there are repoPaths", function(done) {
      var started = 0, finished = 0;
      var cimpler = new Cimpler({
         plugins: {
            "git-build": {
               repoPath: testRepoDirs,
               // Pass if test_branch is the build branch
               cmd: "sleep 1 && [ \"$BUILD_BRANCH\" = 'test-branch' ]",
               logs: {
                  path: buildLogsPath,
                  url:  "http://www.example.com/ci-builds/"
               },
            }
         },
      });

      var check = expect(4, function() {
         cimpler.shutdown();
         done();
      });

      var expectedBuildCommits = {
         A: testBranch,
         B: master
      };
      cimpler.on('buildStarted', function(build) {
         assert.equal(build.sha, expectedBuildCommits[build.letter]);
         check();
         started++;
         // We should be running these in paralell
         assert.ok(finished == 0, "Both builds should start before either finish");
      });

      var expectedStatuses = {
         A: 'success',
         B: 'failure'
      };
      cimpler.on('buildFinished', function(build) {
         assert.equal(build.status, expectedStatuses[build.letter]);
         check();
         finished++;
         // We should be running these in paralell
         assert.ok(started == 2, "One build finished before both has started");
      });

      cimpler.addBuild({
         letter: 'A',
         repo: "doesn't matter",
         branch: "test-branch"
      });
      cimpler.addBuild({
         letter: 'B',
         repo: "doesn't matter",
         branch: "master"
      });
   });

   function exec(cmd, dir, callback, expectFailure) {
      var execOptions = {
         cwd: dir
      };
      childProcess.exec(cmd, execOptions, function(err, stdout, stderr) {
         if (!expectFailure != !err) {
            var msg = err ? "Command failed: " + cmd :
                            "Command was supposed to fail (but didn't): " + cmd;
            util.error(msg);
            console.log(stdout.toString());
            console.log(stderr.toString());
            process.exit(1);
         }
         callback(stdout.toString().trim());
      });
   }

   /**
    * Because Git doesn't allow adding any files or dirs named .git
    * we can't add the test repo's .git dir directly to the main repo.
    * We must resort to dynamically creating and destroying a
    * "symlink-like" file that points git to a different folder for
    * the actual repo dir.
    *
    * Actual repo dir:        fixtures/repo/git
    * "symlink" to repo dir:  fixtures/repo/.git
    */
   before(function(done) {
      // Add a 'git-style' symlink
      fs.writeFileSync(testRepoSource + '.git', "gitdir: ./git");
      // Ignore the "Already exists" errors
      try { fs.mkdirSync(buildLogsPath) } catch (err) {}
      // Clone the test repo into a temp dir.
      var cmd = testRepoDirs.map(function (dir) {
         return "git clone " + testRepoSource + " " + dir;
      }).join(" && ");
      childProcess.exec(cmd, {}, done);
   });

   after(function() {
      fs.unlink(testRepoSource + '.git');
   });
});

function concurrencyChecker(max) {
   var levels = 0;
   return function (inc) {
      levels += inc;
      if (levels > max)
         assert.fail("Concurrency should not be greater than " + max);
   };
}

function tempDir() {
   return "/tmp/cimpler-test-" +
            Math.floor(Math.random() * 999999);
}
<|endoftext|>


<commit_before>const logger = require('winston')
const listItems = require('../model/listitems')

const route = (request, response) => {
  logger.log('info', request)
  const data = {
    list: 'shopping',
    familyId: 1
  }
  listItems.findPromise(data)
    .then(result => {
      const listItemNames = result.listItems.map(listItem => {
        return {listItem: listItem.listItemName}
      })
      response.json(listItemNames)
    })
}

module.exports = {
  route
}
<commit_msg>use correct fam id<commit_after>const logger = require('winston')
const listItems = require('../model/listitems')

const route = (request, response) => {
  logger.log('info', request)
  const data = {
    list: 'shopping',
    familyId: 2
  }
  listItems.findPromise(data)
    .then(result => {
      const listItemNames = result.listItems.map(listItem => {
        return {listItem: listItem.listItemName}
      })
      response.json(listItemNames)
    })
}

module.exports = {
  route
}
<|endoftext|>


<commit_before>const fs = require('fs');
const cp = require('child_process');
const readline = require('readline');
const tmp = require('tmp');
const commandLineArgs = require('command-line-args');

const prefix = 'speech';
const plen = prefix.length;
const optionDefinitions = [
  { name: 'bgonly', alias: 'b', type: Boolean },
  { name: 'destfolder', alias: 'd', type: String },
  { name: 'output', alias: 'o', type: String },
  { name: 'play', alias: 'p', type: String },
  { name: 'soundfolder', alias: 's', type: String },
];
const options = commandLineArgs(optionDefinitions);

var script = options.play;
var soundFolder = options.soundfolder;
var destFolder = options.destfolder;
var bgonly = options.hasOwnProperty('bgonly') ? options.bgonly : false;
var output = options.hasOwnProperty('output') ? options.output : 'result.wav';

if (!options.hasOwnProperty('play')
   || !options.hasOwnProperty('soundfolder')
   || !options.hasOwnProperty('destfolder')) {
  // Can't do anything useful without input
  return;
} else if (destFolder.slice(-1) != '/') {
  destFolder += '/';
}

if (soundFolder.slice(-1) != '/') {
  soundFolder += '/';
}

var files = fs.readdirSync(soundFolder);
var indexName = soundFolder + '/index.txt';
var soundIndex = fs.readFileSync(indexName).toString().split('\n');
soundIndex.forEach(function (line, index, array) {
  soundIndex[index] = line.split('\t');
});

var tmpDir = tmp.dirSync({unsafeCleanup: true});
var backgroundSounds = [];
var lineno = 1;                 // Count lines for easy addressing
var lineReader = readline.createInterface({
  terminal: false,
  input: fs.createReadStream(script),
});
fs.stat('soundeffects.csv', function(err, stat) {
  if (err != null) {
    fs.unlink('soundeffects.csv');
  }
});
lineReader.on('line', function (line) {
  if (line.length == 0) {
    lineno += 1;
    return;
  }
  var init = line[0];
  if (init == '(' || init == '#' || init.toUpperCase() != init) {
    wordno = 1;
    line.split(' ').forEach(function (word, index, array) {
    	if (word[0] == '[') {
    	  var continuing = false;
    	  if (word.slice(-1) == ','
    	   || word.slice(-1) == ';'
    	   || word.slice(-1) == '.'
    	   || word.slice(-1) == ')') {
    	    word = word.slice(0, -1);
    	  }
    	  word = word.slice(1, -1);
    	  if (word.slice(-1) == '~') {
    	    continuing = true;
    	    word = word.slice(0, -1);
    	  }
    	  var sound = fileNameFromCode(soundIndex, word);
    	  var dur = timeFromCode(soundIndex, word);
    	  var ext = sound.split('.').slice(-1);
    	  var lcode = lineno + '-' + wordno;
    	  var outName = 'speech' + lcode + '.' + ext;
    	  copyFile(soundFolder + '/' + sound, destFolder + outName);
          fs.appendFileSync('soundeffects.csv', lcode.toString() + ',' + sound + '\n');
    	  if (continuing) {
    	    backgroundSounds.push(outName);
    	  }
    	}
    	wordno += 1;
    });
  } else if (line.slice(-1) == '^') {
    backgroundSounds.push('speech' + (lineno - 2) + '.wav');
  }
  lineno += 1;
}).on('close', function (err) {
  var timecode = 0.0;
  var empty = cp.spawnSync('/usr/bin/rec', [ output, 'trim', 0, 0 ])
  var channels = 0;
  var brate = 0;
  var srate = 0;
  fs.readdirSync(destFolder).forEach(function (file, index, array) {
    var proc = cp.spawnSync('/usr/bin/soxi', [ '-c', destFolder + file ]);
    var c = parseInt(proc.stdout.toString().trim(), 10);
    proc = cp.spawnSync('/usr/bin/soxi', [ '-b', destFolder + file ]);
    var b = parseInt(proc.stdout.toString().trim(), 10);
    proc = cp.spawnSync('/usr/bin/soxi', [ '-s', destFolder + file ]);
    var s = parseInt(proc.stdout.toString().trim(), 10);
    channels = Math.max(channels, c);
    brate = Math.max(brate, b);
    srate = Math.max(srate, s);
  });
  if (srate > 48000) {
    srate = 48000;
  }
  fs.readdirSync(destFolder).sort(audioFileSort).forEach(function (file, index, array) {
    var background = false;
    backgroundSounds.forEach(function (name, index, array) {
      if (file == name) {
        background = true;
      }
    });
    var normalize = cp.spawnSync('/usr/bin/sox',
      [ '--norm', destFolder + file, '--bits', brate, '--channels', channels,
        tmpDir.name + '/' + file, 'rate', '-v', '-s', srate ]);
    fs.renameSync(tmpDir.name + '/' + file, destFolder + file);
    var timing = cp.spawnSync('/usr/bin/soxi', [ '-D', destFolder + file ]);
    var length = timing.stdout.toString().trim();
    if (timing.stderr.toString().trim() != '') {
      console.log(file);
      console.log(timing.stderr.toString().trim());
    } else if (background) {
      var silence = cp.spawnSync('/usr/bin/sox',
        [ '-n', '-r', srate, '-c', channels, tmpDir.name + '/empty.wav',
          'trim', '0.0', timecode ]);
      if (silence.stderr.toString().trim()) {
        console.log('silence: ' + silence.stderr.toString().trim());
      }
      var append = cp.spawnSync('/usr/bin/sox',
        [ tmpDir.name + '/empty.wav',
          destFolder + '/' + file,
          tmpDir.name + '/' + file ]);
      if (append.stderr.toString().trim()) {
        console.log('prepend silence: ' + silence.stderr.toString().trim());
      } else {
        fs.unlinkSync(tmpDir.name + '/empty.wav');
      }
      console.log(file + ' offset by ' + timecode + ' seconds');
    } else if (!bgonly) {
      var dur = parseFloat(timing.stdout.toString().trim(), 10);
      timecode += dur;
      var append = cp.spawnSync('/usr/bin/sox',
        [ '--norm', output, destFolder + file, tmpDir.name + '/' + output ]);
      fs.renameSync(tmpDir.name + '/' + output, output);
      console.log(file + ' appended, total time at ' + timecode);
    }
  });
  fs.readdirSync(tmpDir.name).sort(audioFileSort).forEach(function (file, index, array) {
    var mix = cp.spawnSync('/usr/bin/sox',
      [ '-m', output, tmpDir.name + '/' + file, tmpDir.name + '/' + output ]);
    if (mix.stderr.toString().trim() != '') {
      console.log(mix.stderr.toString().trim());
    }
    var compand = cp.spawnSync('/usr/bin/sox',
      [ tmpDir.name + '/' + output, tmpDir.name + '/resultA.wav', 'compand',
        '0.3,1', '6:-70,-60,-20', '-5', '-90', '0.2' ]);
    if (compand.stderr.toString().trim() != '') {
      console.log(compand.stderr.toString().trim());
    }
    fs.unlinkSync(tmpDir.name + '/' + output);
    fs.renameSync(tmpDir.name + '/resultA.wav', output);
    console.log(file + ' mixed');
  });
  console.log(timecode + ' seconds total');
});

function audioFileSort(a, b) {
	var aNames = a.slice(plen).split('-');
	var bNames = b.slice(plen).split('-');
	aNames.forEach(function (n, i, a) { aNames[i] = parseInt(n, 10); });
	bNames.forEach(function (n, i, a) { bNames[i] = parseInt(n, 10); });
	if (aNames[0] > bNames[0]) {
		return 1;
	} else if (aNames[0] < bNames[0]) {
		return -1;
	} else if (aNames.length == 1) {
		return 0;
	} else if (aNames[1] > bNames[1]) {
		return 1;
	} else if (aNames[1] < bNames[1]) {
		return -1;
	} else {
		return 0;
	}
}

function fileNameFromCode(index, code) {
  var out = null;
  index.forEach(function (item, index, array) {
    if (item[0] == code.toString() || item[1] == code) {
      out = item[4];
    }
  });
  if (out == null) {
    console.log(code);
  }
  return out;
}

function timeFromCode(index, code) {
  var out = null;
  index.forEach(function (item, index, array) {
    if (item[0] == code.toString() || item[1] == code) {
      out = item[3];
    }
  });
  if (out == null) {
    console.log(code);
  }
  return out;
}

function copyFile(srcFile, destFile) {
  var BUF_LENGTH, buff, bytesRead, fdr, fdw, pos;
  BUF_LENGTH = 64 * 1024;
  buff = new Buffer(BUF_LENGTH);
  fdr = fs.openSync(srcFile, 'r');
  fdw = fs.openSync(destFile, 'w');
  bytesRead = 1;
  pos = 0;
  while (bytesRead > 0) {
    bytesRead = fs.readSync(fdr, buff, 0, BUF_LENGTH, pos);
    fs.writeSync(fdw, buff, 0, bytesRead);
    pos += bytesRead;
  }
  fs.closeSync(fdr);
  return fs.closeSync(fdw);
}

<commit_msg>Add sound effect tag to table for easier downstream processing<commit_after>const fs = require('fs');
const cp = require('child_process');
const readline = require('readline');
const tmp = require('tmp');
const commandLineArgs = require('command-line-args');

const prefix = 'speech';
const plen = prefix.length;
const optionDefinitions = [
  { name: 'bgonly', alias: 'b', type: Boolean },
  { name: 'destfolder', alias: 'd', type: String },
  { name: 'output', alias: 'o', type: String },
  { name: 'play', alias: 'p', type: String },
  { name: 'soundfolder', alias: 's', type: String },
];
const options = commandLineArgs(optionDefinitions);

var script = options.play;
var soundFolder = options.soundfolder;
var destFolder = options.destfolder;
var bgonly = options.hasOwnProperty('bgonly') ? options.bgonly : false;
var output = options.hasOwnProperty('output') ? options.output : 'result.wav';

if (!options.hasOwnProperty('play')
   || !options.hasOwnProperty('soundfolder')
   || !options.hasOwnProperty('destfolder')) {
  // Can't do anything useful without input
  return;
} else if (destFolder.slice(-1) != '/') {
  destFolder += '/';
}

if (soundFolder.slice(-1) != '/') {
  soundFolder += '/';
}

var files = fs.readdirSync(soundFolder);
var indexName = soundFolder + '/index.txt';
var soundIndex = fs.readFileSync(indexName).toString().split('\n');
soundIndex.forEach(function (line, index, array) {
  soundIndex[index] = line.split('\t');
});

var tmpDir = tmp.dirSync({unsafeCleanup: true});
var backgroundSounds = [];
var lineno = 1;                 // Count lines for easy addressing
var lineReader = readline.createInterface({
  terminal: false,
  input: fs.createReadStream(script),
});
fs.stat('soundeffects.csv', function(err, stat) {
  if (err != null) {
    fs.unlink('soundeffects.csv');
  }
});
lineReader.on('line', function (line) {
  if (line.length == 0) {
    lineno += 1;
    return;
  }
  var init = line[0];
  if (init == '(' || init == '#' || init.toUpperCase() != init) {
    wordno = 1;
    line.split(' ').forEach(function (word, index, array) {
    	if (word[0] == '[') {
    	  var continuing = false;
    	  if (word.slice(-1) == ','
    	   || word.slice(-1) == ';'
    	   || word.slice(-1) == '.'
    	   || word.slice(-1) == ')') {
    	    word = word.slice(0, -1);
    	  }
    	  word = word.slice(1, -1);
    	  if (word.slice(-1) == '~') {
    	    continuing = true;
    	    word = word.slice(0, -1);
    	  }
    	  var sound = fileNameFromCode(soundIndex, word);
    	  var dur = timeFromCode(soundIndex, word);
    	  var ext = sound.split('.').slice(-1);
    	  var lcode = lineno + '-' + wordno;
    	  var outName = 'speech' + lcode + '.' + ext;
    	  copyFile(soundFolder + '/' + sound, destFolder + outName);
          fs.appendFileSync('soundeffects.csv', lcode.toString() + ',' + word + ',' + sound + '\n');
    	  if (continuing) {
    	    backgroundSounds.push(outName);
    	  }
    	}
    	wordno += 1;
    });
  } else if (line.slice(-1) == '^') {
    backgroundSounds.push('speech' + (lineno - 2) + '.wav');
  }
  lineno += 1;
}).on('close', function (err) {
  var timecode = 0.0;
  var empty = cp.spawnSync('/usr/bin/rec', [ output, 'trim', 0, 0 ])
  var channels = 0;
  var brate = 0;
  var srate = 0;
  fs.readdirSync(destFolder).forEach(function (file, index, array) {
    var proc = cp.spawnSync('/usr/bin/soxi', [ '-c', destFolder + file ]);
    var c = parseInt(proc.stdout.toString().trim(), 10);
    proc = cp.spawnSync('/usr/bin/soxi', [ '-b', destFolder + file ]);
    var b = parseInt(proc.stdout.toString().trim(), 10);
    proc = cp.spawnSync('/usr/bin/soxi', [ '-s', destFolder + file ]);
    var s = parseInt(proc.stdout.toString().trim(), 10);
    channels = Math.max(channels, c);
    brate = Math.max(brate, b);
    srate = Math.max(srate, s);
  });
  if (srate > 48000) {
    srate = 48000;
  }
  fs.readdirSync(destFolder).sort(audioFileSort).forEach(function (file, index, array) {
    var background = false;
    backgroundSounds.forEach(function (name, index, array) {
      if (file == name) {
        background = true;
      }
    });
    var normalize = cp.spawnSync('/usr/bin/sox',
      [ '--norm', destFolder + file, '--bits', brate, '--channels', channels,
        tmpDir.name + '/' + file, 'rate', '-v', '-s', srate ]);
    fs.renameSync(tmpDir.name + '/' + file, destFolder + file);
    var timing = cp.spawnSync('/usr/bin/soxi', [ '-D', destFolder + file ]);
    var length = timing.stdout.toString().trim();
    if (timing.stderr.toString().trim() != '') {
      console.log(file);
      console.log(timing.stderr.toString().trim());
    } else if (background) {
      var silence = cp.spawnSync('/usr/bin/sox',
        [ '-n', '-r', srate, '-c', channels, tmpDir.name + '/empty.wav',
          'trim', '0.0', timecode ]);
      if (silence.stderr.toString().trim()) {
        console.log('silence: ' + silence.stderr.toString().trim());
      }
      var append = cp.spawnSync('/usr/bin/sox',
        [ tmpDir.name + '/empty.wav',
          destFolder + '/' + file,
          tmpDir.name + '/' + file ]);
      if (append.stderr.toString().trim()) {
        console.log('prepend silence: ' + silence.stderr.toString().trim());
      } else {
        fs.unlinkSync(tmpDir.name + '/empty.wav');
      }
      console.log(file + ' offset by ' + timecode + ' seconds');
    } else if (!bgonly) {
      var dur = parseFloat(timing.stdout.toString().trim(), 10);
      timecode += dur;
      var append = cp.spawnSync('/usr/bin/sox',
        [ '--norm', output, destFolder + file, tmpDir.name + '/' + output ]);
      fs.renameSync(tmpDir.name + '/' + output, output);
      console.log(file + ' appended, total time at ' + timecode);
    }
  });
  fs.readdirSync(tmpDir.name).sort(audioFileSort).forEach(function (file, index, array) {
    var mix = cp.spawnSync('/usr/bin/sox',
      [ '-m', output, tmpDir.name + '/' + file, tmpDir.name + '/' + output ]);
    if (mix.stderr.toString().trim() != '') {
      console.log(mix.stderr.toString().trim());
    }
    var compand = cp.spawnSync('/usr/bin/sox',
      [ tmpDir.name + '/' + output, tmpDir.name + '/resultA.wav', 'compand',
        '0.3,1', '6:-70,-60,-20', '-5', '-90', '0.2' ]);
    if (compand.stderr.toString().trim() != '') {
      console.log(compand.stderr.toString().trim());
    }
    fs.unlinkSync(tmpDir.name + '/' + output);
    fs.renameSync(tmpDir.name + '/resultA.wav', output);
    console.log(file + ' mixed');
  });
  console.log(timecode + ' seconds total');
});

function audioFileSort(a, b) {
	var aNames = a.slice(plen).split('-');
	var bNames = b.slice(plen).split('-');
	aNames.forEach(function (n, i, a) { aNames[i] = parseInt(n, 10); });
	bNames.forEach(function (n, i, a) { bNames[i] = parseInt(n, 10); });
	if (aNames[0] > bNames[0]) {
		return 1;
	} else if (aNames[0] < bNames[0]) {
		return -1;
	} else if (aNames.length == 1) {
		return 0;
	} else if (aNames[1] > bNames[1]) {
		return 1;
	} else if (aNames[1] < bNames[1]) {
		return -1;
	} else {
		return 0;
	}
}

function fileNameFromCode(index, code) {
  var out = null;
  index.forEach(function (item, index, array) {
    if (item[0] == code.toString() || item[1] == code) {
      out = item[4];
    }
  });
  if (out == null) {
    console.log(code);
  }
  return out;
}

function timeFromCode(index, code) {
  var out = null;
  index.forEach(function (item, index, array) {
    if (item[0] == code.toString() || item[1] == code) {
      out = item[3];
    }
  });
  if (out == null) {
    console.log(code);
  }
  return out;
}

function copyFile(srcFile, destFile) {
  var BUF_LENGTH, buff, bytesRead, fdr, fdw, pos;
  BUF_LENGTH = 64 * 1024;
  buff = new Buffer(BUF_LENGTH);
  fdr = fs.openSync(srcFile, 'r');
  fdw = fs.openSync(destFile, 'w');
  bytesRead = 1;
  pos = 0;
  while (bytesRead > 0) {
    bytesRead = fs.readSync(fdr, buff, 0, BUF_LENGTH, pos);
    fs.writeSync(fdw, buff, 0, bytesRead);
    pos += bytesRead;
  }
  fs.closeSync(fdr);
  return fs.closeSync(fdw);
}

<|endoftext|>


<commit_before>import { combineReducers } from 'redux'
import moment from 'moment'
import omit from 'lodash/omit'
import get from 'lodash/get'

import { buildKonnectorError, isKonnectorUserError } from 'lib/konnectors'

import { getTriggerLastJob } from 'ducks/jobs'

// constant
const ACCOUNT_DOCTYPE = 'io.cozy.accounts'
const TRIGGERS_DOCTYPE = 'io.cozy.triggers'
const JOBS_DOCTYPE = 'io.cozy.jobs'

export const ENQUEUE_CONNECTION = 'ENQUEUE_CONNECTION'
export const LAUNCH_TRIGGER = 'LAUNCH_TRIGGER'
export const PURGE_QUEUE = 'PURGE_QUEUE'
export const RECEIVE_DATA = 'RECEIVE_DATA'
export const RECEIVE_NEW_DOCUMENT = 'RECEIVE_NEW_DOCUMENT'
export const RECEIVE_DELETED_DOCUMENT = 'RECEIVE_DELETED_DOCUMENT'
export const UPDATE_CONNECTION_RUNNING_STATUS =
  'UPDATE_CONNECTION_RUNNING_STATUS'
export const UPDATE_CONNECTION_ERROR = 'UPDATE_CONNECTION_ERROR'
export const START_CONNECTION_CREATION = 'START_CONNECTION_CREATION'
export const END_CONNECTION_CREATION = 'END_CONNECTION_CREATION'

// Helpers
const getTriggerKonnectorSlug = trigger =>
  (trigger && trigger.message && trigger.message.konnector) || null

const isKonnectorTrigger = doc =>
  doc._type === TRIGGERS_DOCTYPE && !!doc.message && !!doc.message.konnector

const isKonnectorJob = doc =>
  doc._type === JOBS_DOCTYPE && doc.worker === 'konnector'

// reducers
const reducer = (state = {}, action) => {
  switch (action.type) {
    case ENQUEUE_CONNECTION:
    case UPDATE_CONNECTION_ERROR:
    case UPDATE_CONNECTION_RUNNING_STATUS:
    case LAUNCH_TRIGGER:
      // Trigger is launched, connection should be running.
      if (!action.trigger || !action.trigger._id)
        throw new Error('Missing trigger id')
      if (!action.trigger.message || !action.trigger.message.konnector)
        throw new Error('Malformed trigger message')
      return {
        ...state,
        [getTriggerKonnectorSlug(action.trigger)]: konnectorReducer(
          state[getTriggerKonnectorSlug(action.trigger)],
          action
        )
      }

    case RECEIVE_DATA:
    case RECEIVE_NEW_DOCUMENT:
      if (
        !action.response ||
        !action.response.data ||
        !action.response.data.length
      ) {
        return state
      }

      return action.response.data.reduce((newState, doc) => {
        const isTrigger = isKonnectorTrigger(doc)
        const isJob = isKonnectorJob(doc)
        // Ignore non triggers or non jobs
        if (!isTrigger && !isJob) return newState
        const konnectorSlug = doc.message.konnector
        const triggerId = (isTrigger && doc._id) || (isJob && doc.trigger_id)
        if (!triggerId) return newState

        const account = isTrigger && !!doc.message && doc.message.account

        const currentStatus =
          (isTrigger && (doc.current_state && doc.current_state.status)) ||
          (isJob && doc.state)

        const error =
          (isTrigger &&
            !!doc.current_state &&
            doc.current_state.status !== 'done' &&
            !!doc.current_state.last_error &&
            buildKonnectorError(doc.current_state.last_error)) ||
          (isJob && !!doc.error && buildKonnectorError(doc.error)) ||
          null

        const lastSyncDate =
          (isTrigger &&
            !!doc.current_state &&
            doc.current_state.last_execution) ||
          (isJob && doc.queued_at)

        const existingTriggers = get(
          newState,
          [konnectorSlug, 'triggers', 'data'],
          []
        )
        let rawTriggers = existingTriggers

        if (isTrigger) {
          rawTriggers = existingTriggers.filter(({ _id }) => _id !== doc._id)
          rawTriggers.push(doc)
        }

        return {
          ...newState,
          [konnectorSlug]: {
            triggers: {
              ...get(newState, [konnectorSlug, 'triggers'], []),
              data: rawTriggers,
              [triggerId]: {
                ...get(newState, [konnectorSlug, 'triggers', triggerId], {}),
                account:
                  account ||
                  get(newState, [
                    konnectorSlug,
                    'triggers',
                    triggerId,
                    'account'
                  ]),
                error,
                hasError: !!error || currentStatus === 'errored',
                isRunning: ['queued', 'running'].includes(currentStatus),
                isConnected: !error && currentStatus === 'done',
                lastSyncDate: lastSyncDate
              }
            }
          }
        }
      }, state)

    case PURGE_QUEUE:
    case RECEIVE_DELETED_DOCUMENT:
      return Object.keys(state).reduce((konnectors, slug) => {
        return {
          ...konnectors,
          [slug]: konnectorReducer(state[slug], action)
        }
      }, state)

    default:
      return state
  }
}

const creation = (state = null, action) => {
  switch (action.type) {
    case RECEIVE_DATA:
    case RECEIVE_NEW_DOCUMENT: {
      if (!state) return null
      if (
        !action.response ||
        !action.response.data ||
        action.response.data.length !== 1
      ) {
        return state
      }

      const doc = action.response.data[0]
      const isAccount = doc._type === ACCOUNT_DOCTYPE

      if (isAccount)
        return {
          ...state,
          account: doc._id
        }

      const isTrigger = isKonnectorTrigger(doc)
      if (isTrigger)
        return {
          ...state,
          trigger: doc._id
        }

      return state
    }
    case START_CONNECTION_CREATION:
      // Store all data related to the creation of a new connection in then
      // property `creation`
      // While a new connection is being configured, new trigger and account
      // are store here
      return {}
    case END_CONNECTION_CREATION:
      return null
    default:
      return state
  }
}

export default combineReducers({
  creation,
  konnectors: reducer
})

// sub(?) reducers
const konnectorReducer = (state = {}, action) => {
  switch (action.type) {
    case ENQUEUE_CONNECTION:
    case LAUNCH_TRIGGER:
    case RECEIVE_DATA:
    case RECEIVE_NEW_DOCUMENT:
    case RECEIVE_DELETED_DOCUMENT:
    case PURGE_QUEUE:
      // We assume that document being a trigger has already been validated.
      return {
        ...state,
        triggers: triggersReducer(state.triggers, action)
      }
    default:
      return state
  }
}

const triggersReducer = (state = {}, action) => {
  switch (action.type) {
    case ENQUEUE_CONNECTION:
      return {
        ...state,
        [action.trigger._id]: {
          ...state[action.trigger._id],
          isEnqueued: true
        }
      }
    case LAUNCH_TRIGGER:
      return {
        ...state,
        [action.trigger._id]: {
          ...state[action.trigger._id],
          account: action.trigger.message.account,
          isRunning: true
        }
      }
    case PURGE_QUEUE:
      return state
        ? Object.keys(state).reduce((newState, triggerId) => {
            return {
              ...newState,
              [triggerId]: {
                ...newState[triggerId],
                isEnqueued: false
              }
            }
          }, state)
        : state
    case RECEIVE_DELETED_DOCUMENT: {
      const { data } = action.response
      const { _id, _type } = data[0]
      if (_type === TRIGGERS_DOCTYPE) {
        return omit(state, _id)
      } else return state
    }
    default:
      return state
  }
}

// action creators sync
export const enqueueConnection = trigger => ({
  type: ENQUEUE_CONNECTION,
  trigger
})

export const purgeQueue = () => ({
  type: PURGE_QUEUE
})

export const updateConnectionError = (konnector, account, error) => ({
  type: UPDATE_CONNECTION_ERROR,
  konnector,
  account,
  error
})

export const startConnectionCreation = konnector => ({
  type: START_CONNECTION_CREATION,
  konnector
})

export const endConnectionCreation = () => ({
  type: END_CONNECTION_CREATION
})

// selectors
export const getConnectionsByKonnector = (
  state,
  konnectorSlug,
  validAccounts = [],
  validKonnectors = []
) => {
  const konnectorIsValid =
    !validKonnectors.length || validKonnectors.includes(konnectorSlug)
  const konnectorHasConnections =
    state.konnectors[konnectorSlug] &&
    Object.keys(state.konnectors[konnectorSlug].triggers).length
  if (!konnectorIsValid || !konnectorHasConnections) return []

  return Object.values(state.konnectors[konnectorSlug].triggers).filter(
    trigger => validAccounts.includes(trigger.account)
  )
}

export const getFirstError = (state, konnectorSlug) => {
  const firstTriggerHavingError =
    !!state.konnectors &&
    !!state.konnectors[konnectorSlug] &&
    !!state.konnectors[konnectorSlug].triggers &&
    Object.values(state.konnectors[konnectorSlug].triggers).find(
      trigger => !!trigger.error
    )
  return !!firstTriggerHavingError && firstTriggerHavingError.error
}

export const getFirstUserError = (state, konnectorSlug) => {
  const firstTriggerHavingUserError =
    !!state.konnectors &&
    !!state.konnectors[konnectorSlug] &&
    !!state.konnectors[konnectorSlug].triggers &&
    Object.values(state.konnectors[konnectorSlug].triggers).find(trigger =>
      isKonnectorUserError(trigger.error)
    )
  return firstTriggerHavingUserError && firstTriggerHavingUserError.error
}

export const getLastSyncDate = (state, konnectorSlug) => {
  const lastExecutions =
    !!state.konnectors &&
    !!state.konnectors[konnectorSlug] &&
    !!state.konnectors[konnectorSlug].triggers &&
    Object.values(state.konnectors[konnectorSlug].triggers)
      .map(trigger => trigger.lastSyncDate)
      .sort((dateA, dateB) => {
        const momentA = moment.utc(dateA)
        const momentB = moment.utc(dateB)
        return momentA.isAfter(momentB) ? -1 : momentA.isBefore(momentB) ? 1 : 0
      })
  return lastExecutions.length && lastExecutions[0]
}

// Map the trigger status to a status compatible with queue
const getTriggerQueueStatus = trigger => {
  if (trigger.isRunning) return 'ongoing'
  if (trigger.hasError) return 'error'
  if (trigger.isConnected) return 'done'
  return 'pending'
}

export const getQueue = (state, konnectors) =>
  // state is state.connections
  state.konnectors
    ? Object.keys(state.konnectors).reduce(
        (queuedConnections, konnectorSlug) => {
          const triggers = state.konnectors[konnectorSlug].triggers
          if (!triggers) return queuedConnections
          const konnector = konnectors[konnectorSlug]
          return queuedConnections.concat(
            Object.keys(triggers).reduce((queuedTriggers, triggerId) => {
              if (triggers[triggerId].isEnqueued) {
                const label = konnector.name
                const status = getTriggerQueueStatus(triggers[triggerId])
                return queuedTriggers.concat({
                  konnector,
                  label,
                  status,
                  triggerId
                })
              }

              return queuedTriggers
            }, [])
          )
        },
        []
      )
    : []

export const getConnectionError = (state, trigger) =>
  getTriggerState(state, trigger).error

export const getCreatedAccount = state =>
  !!state.creation && state.creation.account

export const getTriggerIdByKonnectorAndAccount = (
  state,
  konnector,
  account,
  validAccounts = []
) =>
  !!konnector &&
  !!account &&
  validAccounts.includes(account._id) &&
  !!state.konnectors[konnector.slug] &&
  Object.keys(state.konnectors[konnector.slug].triggers).find(
    triggerId =>
      state.konnectors[konnector.slug].triggers[triggerId].account ===
      account._id
  )

export const getTriggerLastSuccess = (state, trigger) => {
  const lastJob = getTriggerLastJob(state, trigger)
  const lastJobIsSuccess = lastJob && lastJob.state === 'done'
  if (lastJobIsSuccess) return lastJob.started_at
  return (
    !!trigger && !!trigger.current_state && trigger.current_state.last_success
  )
}

// get trigger from state, in state.konnectors[konnectorSlug].triggers[triggerId]
const getTriggerState = (state, trigger) => {
  const konnectorSlug = getTriggerKonnectorSlug(trigger)
  if (!konnectorSlug || !state.konnectors || !state.konnectors[konnectorSlug])
    return false
  const triggers = state.konnectors[konnectorSlug].triggers
  if (!triggers) return false
  return (!!triggers && !!triggers[trigger._id] && triggers[trigger._id]) || {}
}

export const isCreatingConnection = state => !!state.creation

export const isConnectionConnected = (state, trigger) =>
  getTriggerState(state, trigger).isConnected

export const isConnectionEnqueued = (state, trigger) =>
  getTriggerState(state, trigger).isEnqueued

export const isConnectionRunning = (state, trigger) =>
  getTriggerState(state, trigger).isRunning
<commit_msg>refactor: Revert 4 previous commits<commit_after>import { combineReducers } from 'redux'
import moment from 'moment'
import omit from 'lodash/omit'

import { buildKonnectorError, isKonnectorUserError } from 'lib/konnectors'

import { getTriggerLastJob } from 'ducks/jobs'

// constant
const ACCOUNT_DOCTYPE = 'io.cozy.accounts'
const TRIGGERS_DOCTYPE = 'io.cozy.triggers'
const JOBS_DOCTYPE = 'io.cozy.jobs'

export const ENQUEUE_CONNECTION = 'ENQUEUE_CONNECTION'
export const LAUNCH_TRIGGER = 'LAUNCH_TRIGGER'
export const PURGE_QUEUE = 'PURGE_QUEUE'
export const RECEIVE_DATA = 'RECEIVE_DATA'
export const RECEIVE_NEW_DOCUMENT = 'RECEIVE_NEW_DOCUMENT'
export const RECEIVE_DELETED_DOCUMENT = 'RECEIVE_DELETED_DOCUMENT'
export const UPDATE_CONNECTION_RUNNING_STATUS =
  'UPDATE_CONNECTION_RUNNING_STATUS'
export const UPDATE_CONNECTION_ERROR = 'UPDATE_CONNECTION_ERROR'
export const START_CONNECTION_CREATION = 'START_CONNECTION_CREATION'
export const END_CONNECTION_CREATION = 'END_CONNECTION_CREATION'

// Helpers
const getTriggerKonnectorSlug = trigger =>
  (trigger && trigger.message && trigger.message.konnector) || null

const isKonnectorTrigger = doc =>
  doc._type === TRIGGERS_DOCTYPE && !!doc.message && !!doc.message.konnector

const isKonnectorJob = doc =>
  doc._type === JOBS_DOCTYPE && doc.worker === 'konnector'

// reducers
const reducer = (state = {}, action) => {
  switch (action.type) {
    case ENQUEUE_CONNECTION:
    case UPDATE_CONNECTION_ERROR:
    case UPDATE_CONNECTION_RUNNING_STATUS:
    case LAUNCH_TRIGGER:
      // Trigger is launched, connection should be running.
      if (!action.trigger || !action.trigger._id)
        throw new Error('Missing trigger id')
      if (!action.trigger.message || !action.trigger.message.konnector)
        throw new Error('Malformed trigger message')
      return {
        ...state,
        [getTriggerKonnectorSlug(action.trigger)]: konnectorReducer(
          state[getTriggerKonnectorSlug(action.trigger)],
          action
        )
      }

    case RECEIVE_DATA:
    case RECEIVE_NEW_DOCUMENT:
      if (
        !action.response ||
        !action.response.data ||
        !action.response.data.length
      ) {
        return state
      }

      return action.response.data.reduce((newState, doc) => {
        const isTrigger = isKonnectorTrigger(doc)
        const isJob = isKonnectorJob(doc)
        // Ignore non triggers or non jobs
        if (!isTrigger && !isJob) return newState
        const konnectorSlug = doc.message.konnector
        const triggerId = (isTrigger && doc._id) || (isJob && doc.trigger_id)
        if (!triggerId) return newState

        const account = isTrigger && !!doc.message && doc.message.account

        const currentStatus =
          (isTrigger && (doc.current_state && doc.current_state.status)) ||
          (isJob && doc.state)

        const error =
          (isTrigger &&
            !!doc.current_state &&
            doc.current_state.status !== 'done' &&
            !!doc.current_state.last_error &&
            buildKonnectorError(doc.current_state.last_error)) ||
          (isJob && !!doc.error && buildKonnectorError(doc.error)) ||
          null

        const lastSyncDate =
          (isTrigger &&
            !!doc.current_state &&
            doc.current_state.last_execution) ||
          (isJob && doc.queued_at)

        return {
          ...newState,
          [konnectorSlug]: {
            triggers: {
              ...((newState[konnectorSlug] &&
                newState[konnectorSlug].triggers) ||
                {}),
              [triggerId]: {
                ...((newState[konnectorSlug] &&
                  newState[konnectorSlug].triggers &&
                  newState[konnectorSlug].triggers[triggerId]) ||
                  {}),
                account:
                  account ||
                  (newState[konnectorSlug] &&
                    newState[konnectorSlug].triggers &&
                    newState[konnectorSlug].triggers[triggerId] &&
                    newState[konnectorSlug].triggers[triggerId].account),
                error,
                hasError: !!error || currentStatus === 'errored',
                isRunning: ['queued', 'running'].includes(currentStatus),
                isConnected: !error && currentStatus === 'done',
                lastSyncDate: lastSyncDate
              }
            }
          }
        }
      }, state)

    case PURGE_QUEUE:
    case RECEIVE_DELETED_DOCUMENT:
      return Object.keys(state).reduce((konnectors, slug) => {
        return {
          ...konnectors,
          [slug]: konnectorReducer(state[slug], action)
        }
      }, state)

    default:
      return state
  }
}

const creation = (state = null, action) => {
  switch (action.type) {
    case RECEIVE_DATA:
    case RECEIVE_NEW_DOCUMENT: {
      if (!state) return null
      if (
        !action.response ||
        !action.response.data ||
        action.response.data.length !== 1
      ) {
        return state
      }

      const doc = action.response.data[0]
      const isAccount = doc._type === ACCOUNT_DOCTYPE

      if (isAccount)
        return {
          ...state,
          account: doc._id
        }

      const isTrigger = isKonnectorTrigger(doc)
      if (isTrigger)
        return {
          ...state,
          trigger: doc._id
        }

      return state
    }
    case START_CONNECTION_CREATION:
      // Store all data related to the creation of a new connection in then
      // property `creation`
      // While a new connection is being configured, new trigger and account
      // are store here
      return {}
    case END_CONNECTION_CREATION:
      return null
    default:
      return state
  }
}

export default combineReducers({
  creation,
  konnectors: reducer
})

// sub(?) reducers
const konnectorReducer = (state = {}, action) => {
  switch (action.type) {
    case ENQUEUE_CONNECTION:
    case LAUNCH_TRIGGER:
    case RECEIVE_DATA:
    case RECEIVE_NEW_DOCUMENT:
    case RECEIVE_DELETED_DOCUMENT:
    case PURGE_QUEUE:
      // We assume that document being a trigger has already been validated.
      return {
        ...state,
        triggers: triggersReducer(state.triggers, action)
      }
    default:
      return state
  }
}

const triggersReducer = (state = {}, action) => {
  switch (action.type) {
    case ENQUEUE_CONNECTION:
      return {
        ...state,
        [action.trigger._id]: {
          ...state[action.trigger._id],
          isEnqueued: true
        }
      }
    case LAUNCH_TRIGGER:
      return {
        ...state,
        [action.trigger._id]: {
          ...state[action.trigger._id],
          account: action.trigger.message.account,
          isRunning: true
        }
      }
    case PURGE_QUEUE:
      return state
        ? Object.keys(state).reduce((newState, triggerId) => {
            return {
              ...newState,
              [triggerId]: {
                ...newState[triggerId],
                isEnqueued: false
              }
            }
          }, state)
        : state
    case RECEIVE_DELETED_DOCUMENT: {
      const { data } = action.response
      const { _id, _type } = data[0]
      if (_type === TRIGGERS_DOCTYPE) {
        return omit(state, _id)
      } else return state
    }
    default:
      return state
  }
}

// action creators sync
export const enqueueConnection = trigger => ({
  type: ENQUEUE_CONNECTION,
  trigger
})

export const purgeQueue = () => ({
  type: PURGE_QUEUE
})

export const updateConnectionError = (konnector, account, error) => ({
  type: UPDATE_CONNECTION_ERROR,
  konnector,
  account,
  error
})

export const startConnectionCreation = konnector => ({
  type: START_CONNECTION_CREATION,
  konnector
})

export const endConnectionCreation = () => ({
  type: END_CONNECTION_CREATION
})

// selectors
export const getConnectionsByKonnector = (
  state,
  konnectorSlug,
  validAccounts = [],
  validKonnectors = []
) => {
  const konnectorIsValid =
    !validKonnectors.length || validKonnectors.includes(konnectorSlug)
  const konnectorHasConnections =
    state.konnectors[konnectorSlug] &&
    Object.keys(state.konnectors[konnectorSlug].triggers).length
  if (!konnectorIsValid || !konnectorHasConnections) return []

  return Object.values(state.konnectors[konnectorSlug].triggers).filter(
    trigger => validAccounts.includes(trigger.account)
  )
}

export const getFirstError = (state, konnectorSlug) => {
  const firstTriggerHavingError =
    !!state.konnectors &&
    !!state.konnectors[konnectorSlug] &&
    !!state.konnectors[konnectorSlug].triggers &&
    Object.values(state.konnectors[konnectorSlug].triggers).find(
      trigger => !!trigger.error
    )
  return !!firstTriggerHavingError && firstTriggerHavingError.error
}

export const getFirstUserError = (state, konnectorSlug) => {
  const firstTriggerHavingUserError =
    !!state.konnectors &&
    !!state.konnectors[konnectorSlug] &&
    !!state.konnectors[konnectorSlug].triggers &&
    Object.values(state.konnectors[konnectorSlug].triggers).find(trigger =>
      isKonnectorUserError(trigger.error)
    )
  return firstTriggerHavingUserError && firstTriggerHavingUserError.error
}

export const getLastSyncDate = (state, konnectorSlug) => {
  const lastExecutions =
    !!state.konnectors &&
    !!state.konnectors[konnectorSlug] &&
    !!state.konnectors[konnectorSlug].triggers &&
    Object.values(state.konnectors[konnectorSlug].triggers)
      .map(trigger => trigger.lastSyncDate)
      .sort((dateA, dateB) => {
        const momentA = moment.utc(dateA)
        const momentB = moment.utc(dateB)
        return momentA.isAfter(momentB) ? -1 : momentA.isBefore(momentB) ? 1 : 0
      })
  return lastExecutions.length && lastExecutions[0]
}

// Map the trigger status to a status compatible with queue
const getTriggerQueueStatus = trigger => {
  if (trigger.isRunning) return 'ongoing'
  if (trigger.hasError) return 'error'
  if (trigger.isConnected) return 'done'
  return 'pending'
}

export const getQueue = (state, konnectors) =>
  // state is state.connections
  state.konnectors
    ? Object.keys(state.konnectors).reduce(
        (queuedConnections, konnectorSlug) => {
          const triggers = state.konnectors[konnectorSlug].triggers
          if (!triggers) return queuedConnections
          const konnector = konnectors[konnectorSlug]
          return queuedConnections.concat(
            Object.keys(triggers).reduce((queuedTriggers, triggerId) => {
              if (triggers[triggerId].isEnqueued) {
                const label = konnector.name
                const status = getTriggerQueueStatus(triggers[triggerId])
                return queuedTriggers.concat({
                  konnector,
                  label,
                  status,
                  triggerId
                })
              }

              return queuedTriggers
            }, [])
          )
        },
        []
      )
    : []

export const getConnectionError = (state, trigger) =>
  getTriggerState(state, trigger).error

export const getCreatedAccount = state =>
  !!state.creation && state.creation.account

export const getTriggerIdByKonnectorAndAccount = (
  state,
  konnector,
  account,
  validAccounts = []
) =>
  !!konnector &&
  !!account &&
  validAccounts.includes(account._id) &&
  !!state.konnectors[konnector.slug] &&
  Object.keys(state.konnectors[konnector.slug].triggers).find(
    triggerId =>
      state.konnectors[konnector.slug].triggers[triggerId].account ===
      account._id
  )

export const getTriggerLastSuccess = (state, trigger) => {
  const lastJob = getTriggerLastJob(state, trigger)
  const lastJobIsSuccess = lastJob && lastJob.state === 'done'
  if (lastJobIsSuccess) return lastJob.started_at
  return (
    !!trigger && !!trigger.current_state && trigger.current_state.last_success
  )
}

// get trigger from state, in state.konnectors[konnectorSlug].triggers[triggerId]
const getTriggerState = (state, trigger) => {
  const konnectorSlug = getTriggerKonnectorSlug(trigger)
  if (!konnectorSlug || !state.konnectors || !state.konnectors[konnectorSlug])
    return false
  const triggers = state.konnectors[konnectorSlug].triggers
  if (!triggers) return false
  return (!!triggers && !!triggers[trigger._id] && triggers[trigger._id]) || {}
}

export const isCreatingConnection = state => !!state.creation

export const isConnectionConnected = (state, trigger) =>
  getTriggerState(state, trigger).isConnected

export const isConnectionEnqueued = (state, trigger) =>
  getTriggerState(state, trigger).isEnqueued

export const isConnectionRunning = (state, trigger) =>
  getTriggerState(state, trigger).isRunning
<|endoftext|>


<commit_before>/**
 * grimoire-skill
 *
 * @license MIT
 * @copyright Voxable 2016
 */

'use strict';

/*eslint-env node, mocha*/

let expect = require( 'chai' ).expect;
let Skill = require( '../' );

describe( 'Skill', function() {
  describe( 'constructor', function() {
    it( 'sets the name to the value of the first argument', function() {
      let skillName = 'sample';
      let subject = new Skill( skillName );

      expect( subject.name ).to.equal( skillName );
    } );
  } );
} );
<commit_msg>Add some vertical space to the tests<commit_after>/**
 * grimoire-skill
 *
 * @license MIT
 * @copyright Voxable 2016
 */

'use strict';

/*eslint-env node, mocha*/

let expect = require( 'chai' ).expect;
let Skill = require( '../' );

describe( 'Skill', function() {

  describe( 'constructor', function() {
    it( 'sets the name to the value of the first argument', function() {
      let skillName = 'sample';
      let subject = new Skill( skillName );

      expect( subject.name ).to.equal( skillName );
    } );
  } );

} );
<|endoftext|>