<commit_before>/*
 * # Semantic - Sidebar
 * http://github.com/jlukic/semantic-ui/
 *
 *
 * Copyright 2013 Contributors
 * Released under the MIT license
 * http://opensource.org/licenses/MIT
 *
 */

;(function ( $, window, document, undefined ) {

$.fn.sidebar = function(parameters) {
  var
    $allModules    = $(this),
    $body          = $('body'),
    $head          = $('head'),

    moduleSelector = $allModules.selector || '',

    time           = new Date().getTime(),
    performance    = [],

    query          = arguments[0],
    methodInvoked  = (typeof query == 'string'),
    queryArguments = [].slice.call(arguments, 1),
    returnedValue
  ;

  $allModules
    .each(function() {
      var
        settings        = ( $.isPlainObject(parameters) )
          ? $.extend(true, {}, $.fn.sidebar.settings, parameters)
          : $.extend({}, $.fn.sidebar.settings),

        selector        = settings.selector,
        className       = settings.className,
        namespace       = settings.namespace,
        error           = settings.error,

        eventNamespace  = '.' + namespace,
        moduleNamespace = 'module-' + namespace,

        $module         = $(this),
        $style          = $('style[title=' + namespace + ']'),

        element         = this,
        instance        = $module.data(moduleNamespace),
        module
      ;

      module      = {

        initialize: function() {
          module.debug('Initializing sidebar', $module);
          module.instantiate();
        },

        instantiate: function() {
          module.verbose('Storing instance of module', module);
          instance = module;
          $module
            .data(moduleNamespace, module)
          ;
        },

        destroy: function() {
          module.verbose('Destroying previous module for', $module);
          $module
            .off(eventNamespace)
            .removeData(moduleNamespace)
          ;
        },

        refresh: function() {
          module.verbose('Refreshing selector cache');
          $style  = $('style[title=' + namespace + ']');
        },

        attachEvents: function(selector, event) {
          var
            $toggle = $(selector)
          ;
          event = $.isFunction(module[event])
            ? module[event]
            : module.toggle
          ;
          if($toggle.size() > 0) {
            module.debug('Attaching sidebar events to element', selector, event);
            $toggle
              .off(eventNamespace)
              .on('click' + eventNamespace, event)
            ;
          }
          else {
            module.error(error.notFound);
          }
        },

        show: function(callback) {
          callback = $.isFunction(callback)
            ? callback
            : function(){}
          ;
          module.debug('Showing sidebar', callback);
          if(module.is.closed()) {
            if(!settings.overlay) {
              if(settings.exclusive) {
                module.hideAll();
              }
              module.pushPage();
            }
            module.set.active();
            callback();
            $.proxy(settings.onChange, element)();
            $.proxy(settings.onShow, element)();
          }
          else {
            module.debug('Sidebar is already visible');
          }
        },

        hide: function(callback) {
          callback = $.isFunction(callback)
            ? callback
            : function(){}
          ;
          module.debug('Hiding sidebar', callback);
          if(module.is.open()) {
            if(!settings.overlay) {
              module.pullPage();
              module.remove.pushed();
            }
            module.remove.active();
            callback();
            $.proxy(settings.onChange, element)();
            $.proxy(settings.onHide, element)();
          }
        },

        hideAll: function() {
          $(selector.sidebar)
            .filter(':visible')
              .sidebar('hide')
          ;
        },

        toggle: function() {
          if(module.is.closed()) {
            module.show();
          }
          else {
            module.hide();
          }
        },

        pushPage: function() {
          var
            direction = module.get.direction(),
            distance  = (module.is.vertical())
              ? $module.outerHeight()
              : $module.outerWidth()
          ;
          if(settings.useCSS) {
            module.debug('Using CSS to animate body');
            module.add.bodyCSS(direction, distance);
            module.set.pushed();
          }
          else {
            module.animatePage(direction, distance, module.set.pushed);
          }
        },

        pullPage: function() {
          var
            direction = module.get.direction()
          ;
          if(settings.useCSS) {
            module.debug('Resetting body position css');
            module.remove.bodyCSS();
          }
          else {
            module.debug('Resetting body position using javascript');
            module.animatePage(direction, 0);
          }
          module.remove.pushed();
        },

        animatePage: function(direction, distance) {
          var
            animateSettings = {}
          ;
          animateSettings['padding-' + direction] = distance;
          module.debug('Using javascript to animate body', animateSettings);
          $body
            .animate(animateSettings, settings.duration, module.set.pushed)
          ;
        },

        add: {
          bodyCSS: function(direction, distance) {
            var
              style
            ;
            if (direction !== className.bottom) {
              var requirePushFix = module.is.touchDevice() && module.is.rtl() && direction == "right";

              style = ''
                + '<style title="' + namespace + '">'
                +  (requirePushFix ? 'html { direction:ltr;} ' : '')
                + 'body.pushed {'
              ;

              if (requirePushFix)
                style +=' margin-left: ' + (-1 * distance) + 'px !important;'
                      + ' padding-' + direction + ': ' + distance + 'px !important;';
              else
                style +=' margin-' + direction + ': ' + distance + 'px !important;';

              style += '}'
                + '</style>'
              ;
            }
            $head.append(style);
            module.debug('Adding body css to head', $style);
          }
        },

        remove: {
          bodyCSS: function() {
            module.debug('Removing body css styles', $style);
            module.refresh();
            $style.remove();
          },
          active: function() {
            $module.removeClass(className.active);
          },
          pushed: function() {
            module.verbose('Removing body push state', module.get.direction());
            $body
              .removeClass(className[ module.get.direction() ])
              .removeClass(className.pushed)
            ;
          }
        },

        set: {
          active: function() {
            $module.addClass(className.active);
          },
          pushed: function() {
            module.verbose('Adding body push state', module.get.direction());
            $body
              .addClass(className[ module.get.direction() ])
              .addClass(className.pushed)
            ;
          }
        },

        get: {
          direction: function() {
            if($module.hasClass(className.top)) {
              return className.top;
            }
            else if($module.hasClass(className.right)) {
              return module.is.rtl() ? className.left : className.right;
            }
            else if($module.hasClass(className.bottom)) {
              return className.bottom;
            }
            else {
              return module.is.rtl() ? className.right : className.left;
            }
          },
          transitionEvent: function() {
            var
              element     = document.createElement('element'),
              transitions = {
                'transition'       :'transitionend',
                'OTransition'      :'oTransitionEnd',
                'MozTransition'    :'transitionend',
                'WebkitTransition' :'webkitTransitionEnd'
              },
              transition
            ;
            for(transition in transitions){
              if( element.style[transition] !== undefined ){
                return transitions[transition];
              }
            }
          }
        },

        is: {
          open: function() {
            return $module.is(':animated') || $module.hasClass(className.active);
          },
          closed: function() {
            return !module.is.open();
          },
          vertical: function() {
            return $module.hasClass(className.top);
          },
          rtl: function () {
            return $module.css('direction') == 'rtl';
          },
          touchDevice: function () {
            return 'ontouchstart' in document.documentElement;
          }
        },

        setting: function(name, value) {
          if( $.isPlainObject(name) ) {
            $.extend(true, settings, name);
          }
          else if(value !== undefined) {
            settings[name] = value;
          }
          else {
            return settings[name];
          }
        },
        internal: function(name, value) {
          if( $.isPlainObject(name) ) {
            $.extend(true, module, name);
          }
          else if(value !== undefined) {
            module[name] = value;
          }
          else {
            return module[name];
          }
        },
        debug: function() {
          if(settings.debug) {
            if(settings.performance) {
              module.performance.log(arguments);
            }
            else {
              module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
              module.debug.apply(console, arguments);
            }
          }
        },
        verbose: function() {
          if(settings.verbose && settings.debug) {
            if(settings.performance) {
              module.performance.log(arguments);
            }
            else {
              module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
              module.verbose.apply(console, arguments);
            }
          }
        },
        error: function() {
          module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
          module.error.apply(console, arguments);
        },
        performance: {
          log: function(message) {
            var
              currentTime,
              executionTime,
              previousTime
            ;
            if(settings.performance) {
              currentTime   = new Date().getTime();
              previousTime  = time || currentTime;
              executionTime = currentTime - previousTime;
              time          = currentTime;
              performance.push({
                'Element'        : element,
                'Name'           : message[0],
                'Arguments'      : [].slice.call(message, 1) || '',
                'Execution Time' : executionTime
              });
            }
            clearTimeout(module.performance.timer);
            module.performance.timer = setTimeout(module.performance.display, 100);
          },
          display: function() {
            var
              title = settings.name + ':',
              totalTime = 0
            ;
            time = false;
            clearTimeout(module.performance.timer);
            $.each(performance, function(index, data) {
              totalTime += data['Execution Time'];
            });
            title += ' ' + totalTime + 'ms';
            if(moduleSelector) {
              title += ' \'' + moduleSelector + '\'';
            }
            if($allModules.size() > 1) {
              title += ' ' + '(' + $allModules.size() + ')';
            }
            if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
              console.groupCollapsed(title);
              if(console.table) {
                console.table(performance);
              }
              else {
                $.each(performance, function(index, data) {
                  console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
                });
              }
              console.groupEnd();
            }
            performance = [];
          }
        },
        invoke: function(query, passedArguments, context) {
          var
            object = instance,
            maxDepth,
            found,
            response
          ;
          passedArguments = passedArguments || queryArguments;
          context         = element         || context;
          if(typeof query == 'string' && object !== undefined) {
            query    = query.split(/[\. ]/);
            maxDepth = query.length - 1;
            $.each(query, function(depth, value) {
              var camelCaseValue = (depth != maxDepth)
                ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
                : query
              ;
              if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
                object = object[camelCaseValue];
              }
              else if( object[camelCaseValue] !== undefined ) {
                found = object[camelCaseValue];
                return false;
              }
              else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
                object = object[value];
              }
              else if( object[value] !== undefined ) {
                found = object[value];
                return false;
              }
              else {
                return false;
              }
            });
          }
          if ( $.isFunction( found ) ) {
            response = found.apply(context, passedArguments);
          }
          else if(found !== undefined) {
            response = found;
          }
          if($.isArray(returnedValue)) {
            returnedValue.push(response);
          }
          else if(returnedValue !== undefined) {
            returnedValue = [returnedValue, response];
          }
          else if(response !== undefined) {
            returnedValue = response;
          }
          return found;
        }
      };
      if(methodInvoked) {
        if(instance === undefined) {
          module.initialize();
        }
        module.invoke(query);
      }
      else {
        if(instance !== undefined) {
          module.destroy();
        }
        module.initialize();
      }
    })
  ;

  return (returnedValue !== undefined)
    ? returnedValue
    : this
  ;
};

$.fn.sidebar.settings = {

  name        : 'Sidebar',
  namespace   : 'sidebar',

  verbose     : true,
  debug       : true,
  performance : true,

  useCSS      : true,
  exclusive   : true,
  overlay     : false,
  duration    : 300,

  onChange     : function(){},
  onShow       : function(){},
  onHide       : function(){},

  className: {
    active : 'active',
    pushed : 'pushed',
    top    : 'top',
    left   : 'left',
    right  : 'right',
    bottom : 'bottom'
  },

  selector: {
    sidebar: '.ui.sidebar'
  },

  error   : {
    method   : 'The method you called is not defined.',
    notFound : 'There were no elements that matched the specified selector'
  }

};

})( jQuery, window , document );
<commit_msg>Fix right sidebar push<commit_after>/*
 * # Semantic - Sidebar
 * http://github.com/jlukic/semantic-ui/
 *
 *
 * Copyright 2013 Contributors
 * Released under the MIT license
 * http://opensource.org/licenses/MIT
 *
 */

;(function ( $, window, document, undefined ) {

$.fn.sidebar = function(parameters) {
  var
    $allModules    = $(this),
    $body          = $('body'),
    $head          = $('head'),

    moduleSelector = $allModules.selector || '',

    time           = new Date().getTime(),
    performance    = [],

    query          = arguments[0],
    methodInvoked  = (typeof query == 'string'),
    queryArguments = [].slice.call(arguments, 1),
    returnedValue
  ;

  $allModules
    .each(function() {
      var
        settings        = ( $.isPlainObject(parameters) )
          ? $.extend(true, {}, $.fn.sidebar.settings, parameters)
          : $.extend({}, $.fn.sidebar.settings),

        selector        = settings.selector,
        className       = settings.className,
        namespace       = settings.namespace,
        error           = settings.error,

        eventNamespace  = '.' + namespace,
        moduleNamespace = 'module-' + namespace,

        $module         = $(this),
        $style          = $('style[title=' + namespace + ']'),

        element         = this,
        instance        = $module.data(moduleNamespace),
        module
      ;

      module      = {

        initialize: function() {
          module.debug('Initializing sidebar', $module);
          module.instantiate();
        },

        instantiate: function() {
          module.verbose('Storing instance of module', module);
          instance = module;
          $module
            .data(moduleNamespace, module)
          ;
        },

        destroy: function() {
          module.verbose('Destroying previous module for', $module);
          $module
            .off(eventNamespace)
            .removeData(moduleNamespace)
          ;
        },

        refresh: function() {
          module.verbose('Refreshing selector cache');
          $style  = $('style[title=' + namespace + ']');
        },

        attachEvents: function(selector, event) {
          var
            $toggle = $(selector)
          ;
          event = $.isFunction(module[event])
            ? module[event]
            : module.toggle
          ;
          if($toggle.size() > 0) {
            module.debug('Attaching sidebar events to element', selector, event);
            $toggle
              .off(eventNamespace)
              .on('click' + eventNamespace, event)
            ;
          }
          else {
            module.error(error.notFound);
          }
        },

        show: function(callback) {
          callback = $.isFunction(callback)
            ? callback
            : function(){}
          ;
          module.debug('Showing sidebar', callback);
          if(module.is.closed()) {
            if(!settings.overlay) {
              if(settings.exclusive) {
                module.hideAll();
              }
              module.pushPage();
            }
            module.set.active();
            callback();
            $.proxy(settings.onChange, element)();
            $.proxy(settings.onShow, element)();
          }
          else {
            module.debug('Sidebar is already visible');
          }
        },

        hide: function(callback) {
          callback = $.isFunction(callback)
            ? callback
            : function(){}
          ;
          module.debug('Hiding sidebar', callback);
          if(module.is.open()) {
            if(!settings.overlay) {
              module.pullPage();
              module.remove.pushed();
            }
            module.remove.active();
            callback();
            $.proxy(settings.onChange, element)();
            $.proxy(settings.onHide, element)();
          }
        },

        hideAll: function() {
          $(selector.sidebar)
            .filter(':visible')
              .sidebar('hide')
          ;
        },

        toggle: function() {
          if(module.is.closed()) {
            module.show();
          }
          else {
            module.hide();
          }
        },

        pushPage: function() {
          var
            direction = module.get.direction(),
            distance  = (module.is.vertical())
              ? $module.outerHeight()
              : $module.outerWidth()
          ;
          if(settings.useCSS) {
            module.debug('Using CSS to animate body');
            module.add.bodyCSS(direction, distance);
            module.set.pushed();
          }
          else {
            module.animatePage(direction, distance, module.set.pushed);
          }
        },

        pullPage: function() {
          var
            direction = module.get.direction()
          ;
          if(settings.useCSS) {
            module.debug('Resetting body position css');
            module.remove.bodyCSS();
          }
          else {
            module.debug('Resetting body position using javascript');
            module.animatePage(direction, 0);
          }
          module.remove.pushed();
        },

        animatePage: function(direction, distance) {
          var
            animateSettings = {}
          ;
          animateSettings['padding-' + direction] = distance;
          module.debug('Using javascript to animate body', animateSettings);
          $body
            .animate(animateSettings, settings.duration, module.set.pushed)
          ;
        },

        add: {
          bodyCSS: function(direction, distance) {
            var
              style
            ;
            if (direction !== className.bottom) {
              var isRTLTouch = module.is.touchDevice() && module.is.rtl();
              var requiresPushFix = isRTLTouch && direction === className.right;
              var requiresLTRDir = isRTLTouch && direction !== className.top;

              style = ''
                + '<style title="' + namespace + '">'
                + (requiresLTRDir ? 'html { direction:ltr;} ' : '')
                + 'body.pushed {'
              ;

              if (requiresPushFix)
                style +=' margin-left: ' + (-1 * distance) + 'px !important;'
                      + ' padding-' + direction + ': ' + distance + 'px !important;';
              else
                style +=' margin-' + direction + ': ' + distance + 'px !important;';

              style += '}'
                + '</style>'
              ;
            }
            $head.append(style);
            module.debug('Adding body css to head', $style);
          }
        },

        remove: {
          bodyCSS: function() {
            module.debug('Removing body css styles', $style);
            module.refresh();
            $style.remove();
          },
          active: function() {
            $module.removeClass(className.active);
          },
          pushed: function() {
            module.verbose('Removing body push state', module.get.direction());
            $body
              .removeClass(className[ module.get.direction() ])
              .removeClass(className.pushed)
            ;
          }
        },

        set: {
          active: function() {
            $module.addClass(className.active);
          },
          pushed: function() {
            module.verbose('Adding body push state', module.get.direction());
            $body
              .addClass(className[ module.get.direction() ])
              .addClass(className.pushed)
            ;
          }
        },

        get: {
          direction: function() {
            if($module.hasClass(className.top)) {
              return className.top;
            }
            else if($module.hasClass(className.right)) {
              return module.is.rtl() ? className.left : className.right;
            }
            else if($module.hasClass(className.bottom)) {
              return className.bottom;
            }
            else {
              return module.is.rtl() ? className.right : className.left;
            }
          },
          transitionEvent: function() {
            var
              element     = document.createElement('element'),
              transitions = {
                'transition'       :'transitionend',
                'OTransition'      :'oTransitionEnd',
                'MozTransition'    :'transitionend',
                'WebkitTransition' :'webkitTransitionEnd'
              },
              transition
            ;
            for(transition in transitions){
              if( element.style[transition] !== undefined ){
                return transitions[transition];
              }
            }
          }
        },

        is: {
          open: function() {
            return $module.is(':animated') || $module.hasClass(className.active);
          },
          closed: function() {
            return !module.is.open();
          },
          vertical: function() {
            return $module.hasClass(className.top);
          },
          rtl: function () {
            return $module.css('direction') == 'rtl';
          },
          touchDevice: function () {
            return 'ontouchstart' in document.documentElement;
          }
        },

        setting: function(name, value) {
          if( $.isPlainObject(name) ) {
            $.extend(true, settings, name);
          }
          else if(value !== undefined) {
            settings[name] = value;
          }
          else {
            return settings[name];
          }
        },
        internal: function(name, value) {
          if( $.isPlainObject(name) ) {
            $.extend(true, module, name);
          }
          else if(value !== undefined) {
            module[name] = value;
          }
          else {
            return module[name];
          }
        },
        debug: function() {
          if(settings.debug) {
            if(settings.performance) {
              module.performance.log(arguments);
            }
            else {
              module.debug = Function.prototype.bind.call(console.info, console, settings.name + ':');
              module.debug.apply(console, arguments);
            }
          }
        },
        verbose: function() {
          if(settings.verbose && settings.debug) {
            if(settings.performance) {
              module.performance.log(arguments);
            }
            else {
              module.verbose = Function.prototype.bind.call(console.info, console, settings.name + ':');
              module.verbose.apply(console, arguments);
            }
          }
        },
        error: function() {
          module.error = Function.prototype.bind.call(console.error, console, settings.name + ':');
          module.error.apply(console, arguments);
        },
        performance: {
          log: function(message) {
            var
              currentTime,
              executionTime,
              previousTime
            ;
            if(settings.performance) {
              currentTime   = new Date().getTime();
              previousTime  = time || currentTime;
              executionTime = currentTime - previousTime;
              time          = currentTime;
              performance.push({
                'Element'        : element,
                'Name'           : message[0],
                'Arguments'      : [].slice.call(message, 1) || '',
                'Execution Time' : executionTime
              });
            }
            clearTimeout(module.performance.timer);
            module.performance.timer = setTimeout(module.performance.display, 100);
          },
          display: function() {
            var
              title = settings.name + ':',
              totalTime = 0
            ;
            time = false;
            clearTimeout(module.performance.timer);
            $.each(performance, function(index, data) {
              totalTime += data['Execution Time'];
            });
            title += ' ' + totalTime + 'ms';
            if(moduleSelector) {
              title += ' \'' + moduleSelector + '\'';
            }
            if($allModules.size() > 1) {
              title += ' ' + '(' + $allModules.size() + ')';
            }
            if( (console.group !== undefined || console.table !== undefined) && performance.length > 0) {
              console.groupCollapsed(title);
              if(console.table) {
                console.table(performance);
              }
              else {
                $.each(performance, function(index, data) {
                  console.log(data['Name'] + ': ' + data['Execution Time']+'ms');
                });
              }
              console.groupEnd();
            }
            performance = [];
          }
        },
        invoke: function(query, passedArguments, context) {
          var
            object = instance,
            maxDepth,
            found,
            response
          ;
          passedArguments = passedArguments || queryArguments;
          context         = element         || context;
          if(typeof query == 'string' && object !== undefined) {
            query    = query.split(/[\. ]/);
            maxDepth = query.length - 1;
            $.each(query, function(depth, value) {
              var camelCaseValue = (depth != maxDepth)
                ? value + query[depth + 1].charAt(0).toUpperCase() + query[depth + 1].slice(1)
                : query
              ;
              if( $.isPlainObject( object[camelCaseValue] ) && (depth != maxDepth) ) {
                object = object[camelCaseValue];
              }
              else if( object[camelCaseValue] !== undefined ) {
                found = object[camelCaseValue];
                return false;
              }
              else if( $.isPlainObject( object[value] ) && (depth != maxDepth) ) {
                object = object[value];
              }
              else if( object[value] !== undefined ) {
                found = object[value];
                return false;
              }
              else {
                return false;
              }
            });
          }
          if ( $.isFunction( found ) ) {
            response = found.apply(context, passedArguments);
          }
          else if(found !== undefined) {
            response = found;
          }
          if($.isArray(returnedValue)) {
            returnedValue.push(response);
          }
          else if(returnedValue !== undefined) {
            returnedValue = [returnedValue, response];
          }
          else if(response !== undefined) {
            returnedValue = response;
          }
          return found;
        }
      };
      if(methodInvoked) {
        if(instance === undefined) {
          module.initialize();
        }
        module.invoke(query);
      }
      else {
        if(instance !== undefined) {
          module.destroy();
        }
        module.initialize();
      }
    })
  ;

  return (returnedValue !== undefined)
    ? returnedValue
    : this
  ;
};

$.fn.sidebar.settings = {

  name        : 'Sidebar',
  namespace   : 'sidebar',

  verbose     : true,
  debug       : true,
  performance : true,

  useCSS      : true,
  exclusive   : true,
  overlay     : false,
  duration    : 300,

  onChange     : function(){},
  onShow       : function(){},
  onHide       : function(){},

  className: {
    active : 'active',
    pushed : 'pushed',
    top    : 'top',
    left   : 'left',
    right  : 'right',
    bottom : 'bottom'
  },

  selector: {
    sidebar: '.ui.sidebar'
  },

  error   : {
    method   : 'The method you called is not defined.',
    notFound : 'There were no elements that matched the specified selector'
  }

};

})( jQuery, window , document );
<|endoftext|>


<commit_before>import Twitter from 'twitter'
import storage from 'electron-json-storage-sync'
import sanitizeHtml from 'sanitize-html'
import autolinker from 'autolinker'
import log4js from 'log4js'
import { credentials, keys } from '../constants'

const oauthInfo = storage.get(keys.OAUTH_TOKEN)
const logger = log4js.getLogger()

// TODO: oauthInfo get failure
const client = new Twitter({
  consumer_key: credentials.CONSUMER_KEY,
  consumer_secret: credentials.CONSUMER_SECRET,
  access_token_key: oauthInfo.data.oauth_access_token,
  access_token_secret: oauthInfo.data.oauth_access_token_secret,
})

export default {

  /**
   * Fetch account
   */
  fetchAccount() {
    return new Promise((resolve, reject) => {
      client.get('account/verify_credentials', (err, user, res) => {
        if (!err) {
          resolve(user)
        } else {
          logger.error(err)
          reject(err)
        }
      })
    })
  },

  /**
   * Fetch list
   */
  fetchLists() {
    return new Promise((resolve, reject) => {
      this.get('lists/list')
        .then((res) => {
          const lists = [{
            id_str: '',
            full_text: '',
          }]
          res.forEach((item) => {
            lists.push(this.parseListItem(item))
          })
          resolve(lists)
        })
        .catch((err) => {
          reject(err)
        })
    })
  },

  /**
   * Fetch tweets
   * @param {string} endpoint
   * @param {Object} params
   */
  fetchTweets(endpoint, params) {
    return new Promise((resolve, reject) => {
      logger.info(`Fetch start ${endpoint}`)
      client.get(endpoint, params, (err, tweets, res) => {
        if (!err) {
          const extendedTweets = []
          tweets.forEach((tweet) => {
            extendedTweets.push(this.parseTweet(tweet))
          })
          logger.info(`Fetch tweets from ${endpoint} count: ${tweets.length}`)
          resolve(extendedTweets)
        } else {
          logger.error(err)
          reject(err)
        }
      })
    })
  },

  /**
   * Post new tweet
   * @param {string} status tweet
   * @param {string} replyScreenName
   * @param {string} inReplyToStatusId
   */
  postTweet(status, replyScreenName, inReplyToStatusId) {
    const params = {}
    params.status = status
    if (inReplyToStatusId !== '' && (status.indexOf(`@${replyScreenName}`) !== -1)) {
      params.in_reply_to_status_id = inReplyToStatusId
    }
    return this.post('statuses/update', params)
  },

  /**
   * Delete tweet
   * @param {string} id
   */
  deleteTweet(id) {
    return this.post('statuses/destroy', {
      id,
    })
  },
  /**
   * Retweet
   * @param {string} id
   */
  retweet(id) {
    return this.post('statuses/retweet', {
      id,
    })
  },

  /**
   * UnRetweet
   * @param {string} id
   */
  unretweet(id) {
    return this.post('statuses/unretweet', {
      id,
    })
  },

  /**
   * Add favorite
   * @param {string} id
   */
  createFavorite(id) {
    return this.post('favorites/create', {
      id,
    })
  },

  /**
   * Remove favorite
   * @param {string} id
   */
  destroyFavorite(id) {
    return this.post('favorites/destroy', {
      id,
    })
  },

  /**
   * get
   * @param {string} url
   * @param {Object} params
   */
  get(url, params) {
    return new Promise((resolve, reject) => {
      if (params == null) {
        client.get(url, (err, items, res) => {
          if (!err) {
            resolve(items)
          } else {
            logger.error(err)
            reject(err)
          }
        })
      } else {
        client.get(url, params, (err, items, res) => {
          if (!err) {
            resolve(items)
          } else {
            logger.error(err)
            reject(err)
          }
        })
      }
    })
  },

  /**
   * post
   * @param {string} url
   * @param {Object} params
   */
  post(url, params) {
    return new Promise((resolve, reject) => {
      client.post(url, params, (err, tweet, res) => {
        if (!err) {
          resolve(tweet)
        } else {
          logger.error(err)
          reject(err)
        }
      })
    })
  },

  /**
   * Tweet object covert to this client object
   * @param {Object} tweet
   */
  parseTweet(tweet) {
    const retw = tweet.retweeted_status
    if (retw) {
      retw.id_str = tweet.id_str
      retw.retweeted_user = tweet.user.name
      tweet = retw
    }
    let html = this.toHtml(tweet.full_text || tweet.text)
    tweet.media_list = []
    if (tweet.entities.urls) {
      html = this.convertURLs(html, tweet.entities.urls)
      Array.prototype.push.apply(tweet.media_list, this.getUrlMedia(tweet.entities.urls))
    }
    if (tweet.extended_entities && tweet.extended_entities.media) {
      html = this.convertURLs(html, tweet.extended_entities.media)
      Array.prototype.push.apply(tweet.media_list, this.getMedia(tweet.extended_entities.media))
    }
    if (tweet.quoted_status) {
      tweet.quoted_status = this.parseTweet(tweet.quoted_status)
    }
    return {
      id_str: tweet.id_str,
      full_text_html: html,
      created_at: tweet.created_at,
      quoted_status: tweet.quoted_status,
      retweeted_user: tweet.retweeted_user,
      favorited: tweet.favorited,
      retweeted: tweet.retweeted,
      media_list: tweet.media_list,
      user: {
        profile_image_url: tweet.user.profile_image_url_https.replace(/_normal/, ''),
        name: tweet.user.name,
        screen_name: tweet.user.screen_name,
        protected: tweet.user.protected,
        verified: tweet.user.verified,
      },
    }
  },

  /**
   * Line feed to br tag and sanitize html
   * @param {string} text
   */
  toHtml(text) {
    text = text.replace(/[\n\r]/g, '
')
    text = sanitizeHtml(text)
    return autolinker.link(text, {
      mention: 'twitter',
      hashtag: 'twitter',
    })
  },

  /**
   * Convert short URL to Real URL
   * @param {string} text tweet
   * @param {Object} urls
   */
  convertURLs(text, urls) {
    for (let ui = 0; ui < urls.length; ui++) {
      text = text.replace(`>${urls[ui].url.replace(/http(|s):\/\//, '')}`, `>${urls[ui].display_url}`)
    }
    return text
  },

  /**
   * Get image or video urls
   * @param {Object} urls
   */
  getUrlMedia(urls) {
    const mediaList = []
    urls.forEach((item) => {
      // instagram
      const shortcode = item.display_url.match(/^instagram\.com\/p\/(.*)\//)
      if (shortcode) {
        mediaList.push({
          url_thumb: `https://instagram.com/p/${shortcode[1]}/media/?size=t`,
          url: `https://instagram.com/p/${shortcode[1]}/media/?size=l`,
        })
      }
      // pixiv
      const illustId = item.expanded_url.match(/.*pixiv\.net.*illust_id=([0-9]+).*/)
      if (illustId) {
        const pixivUrl = `https://embed.pixiv.net/decorate.php?illust_id=${illustId[1]}`
        mediaList.push({
          url_thumb: pixivUrl,
          url: pixivUrl,
        })
      }
    })
    return mediaList
  },

  /**
   * Search extended entities media
   * @param {Object} media
   */
  getMedia(media) {
    const mediaList = []
    media.forEach((item) => {
      const { type } = item
      if (type === 'photo') {
        mediaList.push({
          url_thumb: `${item.media_url}:thumb`,
          url: item.media_url,
        })
      } else if (type === 'video' || type === 'animated_gif') {
        const mp4 = item.video_info.variants.filter(item => (item.content_type === 'video/mp4')).sort((a, b) => ((a.bitrate > b.bitrate) ? -1 : 1))
        if (mp4.length > 0) {
          // Get highest bitrate item
          mediaList.push({
            url_thumb: item.media_url,
            // Remove '?tag=3'
            url: mp4[0].url.replace(/\?.*/, ''),
          })
        }
      }
    })
    return mediaList
  },

  /**
   * Find tweet item for Array.prototype.find
   * @param {Object} tweet
   */
  findItem(tweet) {
    return tweet.id_str === this
  },

  /**
   * list object covert to this client object
   * @param {Object} item
   */
  parseListItem(item) {
    return {
      id_str: item.id_str,
      full_name: item.full_name,
    }
  },
}
<commit_msg>Remove never used vars<commit_after>import Twitter from 'twitter'
import storage from 'electron-json-storage-sync'
import sanitizeHtml from 'sanitize-html'
import autolinker from 'autolinker'
import log4js from 'log4js'
import { credentials, keys } from '../constants'

const oauthInfo = storage.get(keys.OAUTH_TOKEN)
const logger = log4js.getLogger()

// TODO: oauthInfo get failure
const client = new Twitter({
  consumer_key: credentials.CONSUMER_KEY,
  consumer_secret: credentials.CONSUMER_SECRET,
  access_token_key: oauthInfo.data.oauth_access_token,
  access_token_secret: oauthInfo.data.oauth_access_token_secret,
})

export default {

  /**
   * Fetch account
   */
  fetchAccount() {
    return new Promise((resolve, reject) => {
      client.get('account/verify_credentials', (err, user) => {
        if (!err) {
          resolve(user)
        } else {
          logger.error(err)
          reject(err)
        }
      })
    })
  },

  /**
   * Fetch list
   */
  fetchLists() {
    return new Promise((resolve, reject) => {
      this.get('lists/list')
        .then((res) => {
          const lists = [{
            id_str: '',
            full_text: '',
          }]
          res.forEach((item) => {
            lists.push(this.parseListItem(item))
          })
          resolve(lists)
        })
        .catch((err) => {
          reject(err)
        })
    })
  },

  /**
   * Fetch tweets
   * @param {string} endpoint
   * @param {Object} params
   */
  fetchTweets(endpoint, params) {
    return new Promise((resolve, reject) => {
      logger.info(`Fetch start ${endpoint}`)
      client.get(endpoint, params, (err, tweets) => {
        if (!err) {
          const extendedTweets = []
          tweets.forEach((tweet) => {
            extendedTweets.push(this.parseTweet(tweet))
          })
          logger.info(`Fetch tweets from ${endpoint} count: ${tweets.length}`)
          resolve(extendedTweets)
        } else {
          logger.error(err)
          reject(err)
        }
      })
    })
  },

  /**
   * Post new tweet
   * @param {string} status tweet
   * @param {string} replyScreenName
   * @param {string} inReplyToStatusId
   */
  postTweet(status, replyScreenName, inReplyToStatusId) {
    const params = {}
    params.status = status
    if (inReplyToStatusId !== '' && (status.indexOf(`@${replyScreenName}`) !== -1)) {
      params.in_reply_to_status_id = inReplyToStatusId
    }
    return this.post('statuses/update', params)
  },

  /**
   * Delete tweet
   * @param {string} id
   */
  deleteTweet(id) {
    return this.post('statuses/destroy', {
      id,
    })
  },
  /**
   * Retweet
   * @param {string} id
   */
  retweet(id) {
    return this.post('statuses/retweet', {
      id,
    })
  },

  /**
   * UnRetweet
   * @param {string} id
   */
  unretweet(id) {
    return this.post('statuses/unretweet', {
      id,
    })
  },

  /**
   * Add favorite
   * @param {string} id
   */
  createFavorite(id) {
    return this.post('favorites/create', {
      id,
    })
  },

  /**
   * Remove favorite
   * @param {string} id
   */
  destroyFavorite(id) {
    return this.post('favorites/destroy', {
      id,
    })
  },

  /**
   * get
   * @param {string} url
   * @param {Object} params
   */
  get(url, params) {
    return new Promise((resolve, reject) => {
      if (params == null) {
        client.get(url, (err, items) => {
          if (!err) {
            resolve(items)
          } else {
            logger.error(err)
            reject(err)
          }
        })
      } else {
        client.get(url, params, (err, items) => {
          if (!err) {
            resolve(items)
          } else {
            logger.error(err)
            reject(err)
          }
        })
      }
    })
  },

  /**
   * post
   * @param {string} url
   * @param {Object} params
   */
  post(url, params) {
    return new Promise((resolve, reject) => {
      client.post(url, params, (err, tweet) => {
        if (!err) {
          resolve(tweet)
        } else {
          logger.error(err)
          reject(err)
        }
      })
    })
  },

  /**
   * Tweet object covert to this client object
   * @param {Object} tweet
   */
  parseTweet(tweet) {
    const retw = tweet.retweeted_status
    if (retw) {
      retw.id_str = tweet.id_str
      retw.retweeted_user = tweet.user.name
      tweet = retw
    }
    let html = this.toHtml(tweet.full_text || tweet.text)
    tweet.media_list = []
    if (tweet.entities.urls) {
      html = this.convertURLs(html, tweet.entities.urls)
      Array.prototype.push.apply(tweet.media_list, this.getUrlMedia(tweet.entities.urls))
    }
    if (tweet.extended_entities && tweet.extended_entities.media) {
      html = this.convertURLs(html, tweet.extended_entities.media)
      Array.prototype.push.apply(tweet.media_list, this.getMedia(tweet.extended_entities.media))
    }
    if (tweet.quoted_status) {
      tweet.quoted_status = this.parseTweet(tweet.quoted_status)
    }
    return {
      id_str: tweet.id_str,
      full_text_html: html,
      created_at: tweet.created_at,
      quoted_status: tweet.quoted_status,
      retweeted_user: tweet.retweeted_user,
      favorited: tweet.favorited,
      retweeted: tweet.retweeted,
      media_list: tweet.media_list,
      user: {
        profile_image_url: tweet.user.profile_image_url_https.replace(/_normal/, ''),
        name: tweet.user.name,
        screen_name: tweet.user.screen_name,
        protected: tweet.user.protected,
        verified: tweet.user.verified,
      },
    }
  },

  /**
   * Line feed to br tag and sanitize html
   * @param {string} text
   */
  toHtml(text) {
    text = text.replace(/[\n\r]/g, '
')
    text = sanitizeHtml(text)
    return autolinker.link(text, {
      mention: 'twitter',
      hashtag: 'twitter',
    })
  },

  /**
   * Convert short URL to Real URL
   * @param {string} text tweet
   * @param {Object} urls
   */
  convertURLs(text, urls) {
    for (let ui = 0; ui < urls.length; ui++) {
      text = text.replace(`>${urls[ui].url.replace(/http(|s):\/\//, '')}`, `>${urls[ui].display_url}`)
    }
    return text
  },

  /**
   * Get image or video urls
   * @param {Object} urls
   */
  getUrlMedia(urls) {
    const mediaList = []
    urls.forEach((item) => {
      // instagram
      const shortcode = item.display_url.match(/^instagram\.com\/p\/(.*)\//)
      if (shortcode) {
        mediaList.push({
          url_thumb: `https://instagram.com/p/${shortcode[1]}/media/?size=t`,
          url: `https://instagram.com/p/${shortcode[1]}/media/?size=l`,
        })
      }
      // pixiv
      const illustId = item.expanded_url.match(/.*pixiv\.net.*illust_id=([0-9]+).*/)
      if (illustId) {
        const pixivUrl = `https://embed.pixiv.net/decorate.php?illust_id=${illustId[1]}`
        mediaList.push({
          url_thumb: pixivUrl,
          url: pixivUrl,
        })
      }
    })
    return mediaList
  },

  /**
   * Search extended entities media
   * @param {Object} media
   */
  getMedia(media) {
    const mediaList = []
    media.forEach((item) => {
      const { type } = item
      if (type === 'photo') {
        mediaList.push({
          url_thumb: `${item.media_url}:thumb`,
          url: item.media_url,
        })
      } else if (type === 'video' || type === 'animated_gif') {
        const mp4 = item.video_info.variants.filter(item => (item.content_type === 'video/mp4')).sort((a, b) => ((a.bitrate > b.bitrate) ? -1 : 1))
        if (mp4.length > 0) {
          // Get highest bitrate item
          mediaList.push({
            url_thumb: item.media_url,
            // Remove '?tag=3'
            url: mp4[0].url.replace(/\?.*/, ''),
          })
        }
      }
    })
    return mediaList
  },

  /**
   * Find tweet item for Array.prototype.find
   * @param {Object} tweet
   */
  findItem(tweet) {
    return tweet.id_str === this
  },

  /**
   * list object covert to this client object
   * @param {Object} item
   */
  parseListItem(item) {
    return {
      id_str: item.id_str,
      full_name: item.full_name,
    }
  },
}
<|endoftext|>


<commit_before>import {CompositeDisposable, Disposable, File} from 'atom';

import path from 'path';

import React from 'react';
import ReactDom from 'react-dom';
import {autobind} from 'core-decorators';
import compareSets from 'compare-sets';

import WorkdirCache from './models/workdir-cache';
import {nullWorkdirContext} from './models/workdir-context';
import WorkdirContextPool from './models/workdir-context-pool';
import Repository from './models/repository';
import FileSystemChangeObserver from './models/file-system-change-observer';
import WorkspaceChangeObserver from './models/workspace-change-observer';
import ResolutionProgress from './models/conflicts/resolution-progress';
import StyleCalculator from './models/style-calculator';
import RootController from './controllers/root-controller';
import IssueishPaneItem from './atom-items/issueish-pane-item';
import Switchboard from './switchboard';
import yardstick from './yardstick';
import GitTimingsView from './views/git-timings-view';
import AsyncQueue from './async-queue';

const defaultState = {
  resolutionProgressByPath: {},
};

export default class GithubPackage {
  constructor(workspace, project, commandRegistry, notificationManager, tooltips, styles, config, confirm) {
    this.workspace = workspace;
    this.project = project;
    this.commandRegistry = commandRegistry;
    this.notificationManager = notificationManager;
    this.tooltips = tooltips;
    this.config = config;
    this.styles = styles;

    this.styleCalculator = new StyleCalculator(this.styles, this.config);
    this.confirm = confirm;

    this.activeContextQueue = new AsyncQueue();
    this.activeContext = nullWorkdirContext;
    this.workdirCache = new WorkdirCache();
    this.contextPool = new WorkdirContextPool({
      window,
      workspace,
      promptCallback: query => this.controller.promptForCredentials(query),
    });

    this.switchboard = new Switchboard();

    // Handle events from all resident contexts.
    this.subscriptions = new CompositeDisposable(
      this.contextPool.onDidChangeWorkdirOrHead(context => {
        this.refreshAtomGitRepository(context.getWorkingDirectory());
      }),
      this.contextPool.onDidUpdateRepository(context => {
        this.switchboard.didUpdateRepository(context.getRepository());
      }),
      this.contextPool.onDidDestroyRepository(context => {
        if (context === this.activeContext) {
          this.setActiveContext(nullWorkdirContext);
        }
      }),
    );

    this.setupYardstick();
  }

  setupYardstick() {
    const stagingSeries = ['stageLine', 'stageHunk', 'unstageLine', 'unstageHunk'];

    this.subscriptions.add(
      this.switchboard.onDidBeginStageOperation(payload => {
        if (payload.stage && payload.line) {
          yardstick.begin('stageLine');
        } else if (payload.stage && payload.hunk) {
          yardstick.begin('stageHunk');
        } else if (payload.unstage && payload.line) {
          yardstick.begin('unstageLine');
        } else if (payload.unstage && payload.hunk) {
          yardstick.begin('unstageHunk');
        }
      }),
      this.switchboard.onDidUpdateRepository(() => {
        yardstick.mark(stagingSeries, 'update-repository');
      }),
      this.switchboard.onDidFinishRender(context => {
        if (context === 'RootController.showFilePatchForPath') {
          yardstick.finish(stagingSeries);
        }
      }),
    );
  }

  activate(state = {}) {
    this.savedState = {...defaultState, ...state};

    this.subscriptions.add(
      this.project.onDidChangePaths(this.scheduleActiveContextUpdate),
      this.workspace.onDidChangeActivePaneItem(this.scheduleActiveContextUpdate),
      this.styleCalculator.startWatching(
        'github-package-styles',
        ['editor.fontSize', 'editor.fontFamily', 'editor.lineHeight'],
        config => `
          .github-FilePatchView {
            font-size: 1.1em;
          }

          .github-HunkView-line {
            font-size: ${config.get('editor.fontSize')}px;
            font-family: ${config.get('editor.fontFamily')};
            line-height: ${config.get('editor.lineHeight')};
          }
        `,
      ),
      this.workspace.addOpener(uri => {
        if (uri === 'atom-github://debug/timings') {
          return this.createGitTimingsView();
        } else {
          return null;
        }
      }),
      this.workspace.addOpener(IssueishPaneItem.opener),
    );

    this.scheduleActiveContextUpdate(this.savedState);
    this.rerender();
  }

  serialize() {
    const activeRepository = this.getActiveRepository();
    const activeRepositoryPath = activeRepository ? activeRepository.getWorkingDirectoryPath() : null;

    const resolutionProgressByPath = {};
    this.contextPool.withResidentContexts((workdir, context) => {
      const resolutionProgress = context.getResolutionProgress();
      if (!resolutionProgress || resolutionProgress.isEmpty()) {
        return;
      }

      resolutionProgressByPath[workdir] = resolutionProgress.serialize();
    });

    return {
      activeRepositoryPath,
      gitController: this.controller.serialize(),
      resolutionProgressByPath,
    };
  }

  @autobind
  rerender() {
    if (!this.element) {
      this.element = document.createElement('div');
      this.subscriptions.add(new Disposable(() => {
        ReactDom.unmountComponentAtNode(this.element);
        delete this.element;
      }));
    }

    ReactDom.render(
      <RootController
        ref={c => { this.controller = c; }}
        workspace={this.workspace}
        commandRegistry={this.commandRegistry}
        notificationManager={this.notificationManager}
        tooltips={this.tooltips}
        config={this.config}
        confirm={this.confirm}
        activeProjectPath={this.activeProjectPath}
        repository={this.getActiveRepository()}
        resolutionProgress={this.getActiveResolutionProgress()}
        statusBar={this.statusBar}
        savedState={this.savedState.gitController}
        createRepositoryForProjectPath={this.createRepositoryForProjectPath}
        cloneRepositoryForProjectPath={this.cloneRepositoryForProjectPath}
        switchboard={this.switchboard}
      />, this.element,
    );
  }

  async deactivate() {
    this.subscriptions.dispose();

    this.destroyedRepositorySubscription && this.destroyedRepositorySubscription.dispose();
    this.changedRepositorySubscription && this.changedRepositorySubscription.dispose();

    this.contextPool.clear();

    await yardstick.flush();
  }

  consumeStatusBar(statusBar) {
    this.statusBar = statusBar;
    this.rerender();
  }

  createGitTimingsView() {
    return GitTimingsView.createPaneItem();
  }

  createIssueishPaneItem({uri}) {
    return IssueishPaneItem.opener(uri);
  }

  @autobind
  async createRepositoryForProjectPath(projectPath) {
    if (!projectPath) {
      throw new Error('Must specify a project path to create a repository for');
    }
    const repository = await Repository.init(projectPath);
    this.contextPool.add(projectPath, {repository});

    await this.scheduleActiveContextUpdate();
  }

  @autobind
  async cloneRepositoryForProjectPath(remoteUrl, projectPath) {
    const repository = await Repository.clone(remoteUrl, projectPath);
    this.contextPool.add(projectPath, {repository});
    this.projects.addPath(projectPath);

    await this.scheduleActiveContextUpdate();
  }

  getActiveWorkdir() {
    return this.activeContext.getWorkingDirectory();
  }

  getActiveRepository() {
    return this.activeContext.getRepository();
  }

  getActiveResolutionProgress() {
    return this.activeContext.getResolutionProgress();
  }

  @autobind
  async scheduleActiveContextUpdate(savedState = {}) {
    await this.activeContextQueue.push(this.updateActiveModels.bind(this, savedState), {parallel: false});
  }

  /**
   * Derive the git working directory context that should be used for the package's git operations based on the current
   * state of the Atom workspace. In priority, this prefers:
   *
   * - A git working directory that contains the active pane item.
   * - A git working directory corresponding to a single Project.
   * - When initially activating the package, the working directory that was active when the package was last
   *   serialized.
   * - The current context, unchanged, which may be a `NullWorkdirContext`.
   *
   * First updates the pool of resident contexts to match all git working directories that correspond to open
   * projects and pane items.
   */
  async getNextContext(savedState) {
    const candidates = new Set(this.project.getDirectories());

    this.workspace.getPaneItems()
      .map(pathForPaneItem)
      .filter(itemPath => !this.project.contains(itemPath))
      .forEach(itemPath => candidates.add(itemPath));

    const workdirs = await Promise.all(
      Array.from(candidates, candidate => this.workdirCache.find(candidate)),
    );

    this.contextPool.set(workdirs, savedState);

    const activeItemPath = pathForPaneItem(this.workspace.getActivePaneItem());
    if (activeItemPath) {
      // Prefer an active item if it has a git working directory.
      const activeWorkingDir = await this.workdirCache.find(activeItemPath);
      if (activeWorkingDir) { return this.contextPool.getContext(activeWorkingDir); }
    }

    if (this.project.getPaths().length === 1) {
      // Single project
      const activeWorkingDir = await this.workdirCache.find(this.project.getPaths()[0]);
      if (activeWorkingDir) { return this.contextPool.getContext(activeWorkingDir); }
    }

    // Restore models from saved state. Will return a NullWorkdirContext if this path is not presently
    // resident in the pool.
    const savedWorkingDir = savedState.activeRepositoryPath;
    if (savedWorkingDir) {
      return this.contextPool.getContext(savedWorkingDir);
    }

    return this.activeContext;
  }

  setActiveContext(nextActiveContext) {
    if (nextActiveContext !== this.activeContext) {
      this.activeContext = nextActiveContext;
      this.rerender();
    }
  }

  async updateActiveContext(savedState = {}) {
    const nextActiveContext = await this.getNextContext(savedState);
    this.setActiveContext(nextActiveContext);
  }

  refreshAtomGitRepository(workdir) {
    const atomGitRepo = this.project.getRepositories().find(repo => {
      return repo && path.normalize(repo.getWorkingDirectory()) === workdir;
    });
    return atomGitRepo ? atomGitRepo.refreshStatus() : Promise.resolve();
  }
}

function pathForPaneItem(paneItem) {
  if (!paneItem) {
    return null;
  }

  // Likely GitHub package provided pane item
  if (typeof paneItem.getWorkingDirectory === 'function') {
    return paneItem.getWorkingDirectory();
  }

  // TextEditor-like
  if (typeof paneItem.getPath === 'function') {
    return paneItem.getPath();
  }

  // Oh well
  return null;
}
<commit_msg>Right, I renamed that<commit_after>import {CompositeDisposable, Disposable, File} from 'atom';

import path from 'path';

import React from 'react';
import ReactDom from 'react-dom';
import {autobind} from 'core-decorators';
import compareSets from 'compare-sets';

import WorkdirCache from './models/workdir-cache';
import {nullWorkdirContext} from './models/workdir-context';
import WorkdirContextPool from './models/workdir-context-pool';
import Repository from './models/repository';
import FileSystemChangeObserver from './models/file-system-change-observer';
import WorkspaceChangeObserver from './models/workspace-change-observer';
import ResolutionProgress from './models/conflicts/resolution-progress';
import StyleCalculator from './models/style-calculator';
import RootController from './controllers/root-controller';
import IssueishPaneItem from './atom-items/issueish-pane-item';
import Switchboard from './switchboard';
import yardstick from './yardstick';
import GitTimingsView from './views/git-timings-view';
import AsyncQueue from './async-queue';

const defaultState = {
  resolutionProgressByPath: {},
};

export default class GithubPackage {
  constructor(workspace, project, commandRegistry, notificationManager, tooltips, styles, config, confirm) {
    this.workspace = workspace;
    this.project = project;
    this.commandRegistry = commandRegistry;
    this.notificationManager = notificationManager;
    this.tooltips = tooltips;
    this.config = config;
    this.styles = styles;

    this.styleCalculator = new StyleCalculator(this.styles, this.config);
    this.confirm = confirm;

    this.activeContextQueue = new AsyncQueue();
    this.activeContext = nullWorkdirContext;
    this.workdirCache = new WorkdirCache();
    this.contextPool = new WorkdirContextPool({
      window,
      workspace,
      promptCallback: query => this.controller.promptForCredentials(query),
    });

    this.switchboard = new Switchboard();

    // Handle events from all resident contexts.
    this.subscriptions = new CompositeDisposable(
      this.contextPool.onDidChangeWorkdirOrHead(context => {
        this.refreshAtomGitRepository(context.getWorkingDirectory());
      }),
      this.contextPool.onDidUpdateRepository(context => {
        this.switchboard.didUpdateRepository(context.getRepository());
      }),
      this.contextPool.onDidDestroyRepository(context => {
        if (context === this.activeContext) {
          this.setActiveContext(nullWorkdirContext);
        }
      }),
    );

    this.setupYardstick();
  }

  setupYardstick() {
    const stagingSeries = ['stageLine', 'stageHunk', 'unstageLine', 'unstageHunk'];

    this.subscriptions.add(
      this.switchboard.onDidBeginStageOperation(payload => {
        if (payload.stage && payload.line) {
          yardstick.begin('stageLine');
        } else if (payload.stage && payload.hunk) {
          yardstick.begin('stageHunk');
        } else if (payload.unstage && payload.line) {
          yardstick.begin('unstageLine');
        } else if (payload.unstage && payload.hunk) {
          yardstick.begin('unstageHunk');
        }
      }),
      this.switchboard.onDidUpdateRepository(() => {
        yardstick.mark(stagingSeries, 'update-repository');
      }),
      this.switchboard.onDidFinishRender(context => {
        if (context === 'RootController.showFilePatchForPath') {
          yardstick.finish(stagingSeries);
        }
      }),
    );
  }

  activate(state = {}) {
    this.savedState = {...defaultState, ...state};

    this.subscriptions.add(
      this.project.onDidChangePaths(this.scheduleActiveContextUpdate),
      this.workspace.onDidChangeActivePaneItem(this.scheduleActiveContextUpdate),
      this.styleCalculator.startWatching(
        'github-package-styles',
        ['editor.fontSize', 'editor.fontFamily', 'editor.lineHeight'],
        config => `
          .github-FilePatchView {
            font-size: 1.1em;
          }

          .github-HunkView-line {
            font-size: ${config.get('editor.fontSize')}px;
            font-family: ${config.get('editor.fontFamily')};
            line-height: ${config.get('editor.lineHeight')};
          }
        `,
      ),
      this.workspace.addOpener(uri => {
        if (uri === 'atom-github://debug/timings') {
          return this.createGitTimingsView();
        } else {
          return null;
        }
      }),
      this.workspace.addOpener(IssueishPaneItem.opener),
    );

    this.scheduleActiveContextUpdate(this.savedState);
    this.rerender();
  }

  serialize() {
    const activeRepository = this.getActiveRepository();
    const activeRepositoryPath = activeRepository ? activeRepository.getWorkingDirectoryPath() : null;

    const resolutionProgressByPath = {};
    this.contextPool.withResidentContexts((workdir, context) => {
      const resolutionProgress = context.getResolutionProgress();
      if (!resolutionProgress || resolutionProgress.isEmpty()) {
        return;
      }

      resolutionProgressByPath[workdir] = resolutionProgress.serialize();
    });

    return {
      activeRepositoryPath,
      gitController: this.controller.serialize(),
      resolutionProgressByPath,
    };
  }

  @autobind
  rerender() {
    if (!this.element) {
      this.element = document.createElement('div');
      this.subscriptions.add(new Disposable(() => {
        ReactDom.unmountComponentAtNode(this.element);
        delete this.element;
      }));
    }

    ReactDom.render(
      <RootController
        ref={c => { this.controller = c; }}
        workspace={this.workspace}
        commandRegistry={this.commandRegistry}
        notificationManager={this.notificationManager}
        tooltips={this.tooltips}
        config={this.config}
        confirm={this.confirm}
        activeProjectPath={this.activeProjectPath}
        repository={this.getActiveRepository()}
        resolutionProgress={this.getActiveResolutionProgress()}
        statusBar={this.statusBar}
        savedState={this.savedState.gitController}
        createRepositoryForProjectPath={this.createRepositoryForProjectPath}
        cloneRepositoryForProjectPath={this.cloneRepositoryForProjectPath}
        switchboard={this.switchboard}
      />, this.element,
    );
  }

  async deactivate() {
    this.subscriptions.dispose();

    this.destroyedRepositorySubscription && this.destroyedRepositorySubscription.dispose();
    this.changedRepositorySubscription && this.changedRepositorySubscription.dispose();

    this.contextPool.clear();

    await yardstick.flush();
  }

  consumeStatusBar(statusBar) {
    this.statusBar = statusBar;
    this.rerender();
  }

  createGitTimingsView() {
    return GitTimingsView.createPaneItem();
  }

  createIssueishPaneItem({uri}) {
    return IssueishPaneItem.opener(uri);
  }

  @autobind
  async createRepositoryForProjectPath(projectPath) {
    if (!projectPath) {
      throw new Error('Must specify a project path to create a repository for');
    }
    const repository = await Repository.init(projectPath);
    this.contextPool.add(projectPath, {repository});

    await this.scheduleActiveContextUpdate();
  }

  @autobind
  async cloneRepositoryForProjectPath(remoteUrl, projectPath) {
    const repository = await Repository.clone(remoteUrl, projectPath);
    this.contextPool.add(projectPath, {repository});
    this.projects.addPath(projectPath);

    await this.scheduleActiveContextUpdate();
  }

  getActiveWorkdir() {
    return this.activeContext.getWorkingDirectory();
  }

  getActiveRepository() {
    return this.activeContext.getRepository();
  }

  getActiveResolutionProgress() {
    return this.activeContext.getResolutionProgress();
  }

  @autobind
  async scheduleActiveContextUpdate(savedState = {}) {
    await this.activeContextQueue.push(this.updateActiveContext.bind(this, savedState), {parallel: false});
  }

  /**
   * Derive the git working directory context that should be used for the package's git operations based on the current
   * state of the Atom workspace. In priority, this prefers:
   *
   * - A git working directory that contains the active pane item.
   * - A git working directory corresponding to a single Project.
   * - When initially activating the package, the working directory that was active when the package was last
   *   serialized.
   * - The current context, unchanged, which may be a `NullWorkdirContext`.
   *
   * First updates the pool of resident contexts to match all git working directories that correspond to open
   * projects and pane items.
   */
  async getNextContext(savedState) {
    const candidates = new Set(this.project.getDirectories());

    this.workspace.getPaneItems()
      .map(pathForPaneItem)
      .filter(itemPath => !this.project.contains(itemPath))
      .forEach(itemPath => candidates.add(itemPath));

    const workdirs = await Promise.all(
      Array.from(candidates, candidate => this.workdirCache.find(candidate)),
    );

    this.contextPool.set(workdirs, savedState);

    const activeItemPath = pathForPaneItem(this.workspace.getActivePaneItem());
    if (activeItemPath) {
      // Prefer an active item if it has a git working directory.
      const activeWorkingDir = await this.workdirCache.find(activeItemPath);
      if (activeWorkingDir) { return this.contextPool.getContext(activeWorkingDir); }
    }

    if (this.project.getPaths().length === 1) {
      // Single project
      const activeWorkingDir = await this.workdirCache.find(this.project.getPaths()[0]);
      if (activeWorkingDir) { return this.contextPool.getContext(activeWorkingDir); }
    }

    // Restore models from saved state. Will return a NullWorkdirContext if this path is not presently
    // resident in the pool.
    const savedWorkingDir = savedState.activeRepositoryPath;
    if (savedWorkingDir) {
      return this.contextPool.getContext(savedWorkingDir);
    }

    return this.activeContext;
  }

  setActiveContext(nextActiveContext) {
    if (nextActiveContext !== this.activeContext) {
      this.activeContext = nextActiveContext;
      this.rerender();
    }
  }

  async updateActiveContext(savedState = {}) {
    const nextActiveContext = await this.getNextContext(savedState);
    this.setActiveContext(nextActiveContext);
  }

  refreshAtomGitRepository(workdir) {
    const atomGitRepo = this.project.getRepositories().find(repo => {
      return repo && path.normalize(repo.getWorkingDirectory()) === workdir;
    });
    return atomGitRepo ? atomGitRepo.refreshStatus() : Promise.resolve();
  }
}

function pathForPaneItem(paneItem) {
  if (!paneItem) {
    return null;
  }

  // Likely GitHub package provided pane item
  if (typeof paneItem.getWorkingDirectory === 'function') {
    return paneItem.getWorkingDirectory();
  }

  // TextEditor-like
  if (typeof paneItem.getPath === 'function') {
    return paneItem.getPath();
  }

  // Oh well
  return null;
}
<|endoftext|>


<commit_before>'use strict';

// Configuration for the New Relic Node.js agent

exports.config = {
    app_name: process.env.NEW_RELIC_APP_NAME || 'app',
    logging: {
        filepath: process.env.NEW_RELIC_LOG || 'stdout'
    }
};
<commit_msg>Use always "module.exports", not the alias "exports"<commit_after>'use strict';

// Configuration for the New Relic Node.js agent

module.exports.config = {
    app_name: process.env.NEW_RELIC_APP_NAME || 'app',
    logging: {
        filepath: process.env.NEW_RELIC_LOG || 'stdout'
    }
};
<|endoftext|>


<commit_before>/*eslint no-invalid-this: 0*/
/*
 * Maintains a lookup of connected IRC clients. These connections are transient
 * and may be closed for a variety of reasons.
 */
"use strict";
const stats = require("../config/stats");
const log = require("../logging").get("ClientPool");
const Promise = require("bluebird");
const QueuePool = require("../util/QueuePool");

function ClientPool(ircBridge) {
    this._ircBridge = ircBridge;
    // The list of bot clients on servers (not specific users)
    this._botClients = {
        // server_domain: BridgedClient
    };

    // list of virtual users on servers
    this._virtualClients = {
        // server_domain: {
        //    nicks: {
        //      <nickname>: BridgedClient
        //    },
        //    userIds: {
        //      <user_id>: BridgedClient
        //    }
        // }
    }
    // map of numbers of connected clients on each server
    // Counting these is quite expensive because we have to
    // ignore entries where the value is undefined. Instead,
    // just keep track of how many we have.
    this._virtualClientCounts = {
        // server_domain: number
    };

    this._reconnectQueues = {
        // server_domain: QueuePool
    };
}

ClientPool.prototype.killAllClients = function() {
    let domainList = Object.keys(this._virtualClients);
    let clients = [];
    domainList.forEach((domain) => {
        clients = clients.concat(
            Object.keys(this._virtualClients[domain].nicks).map(
                (nick) => this._virtualClients[domain].nicks[nick]
            )
        );

        clients = clients.concat(
            Object.keys(this._virtualClients[domain].userIds).map(
                (userId) => this._virtualClients[domain].userIds[userId]
            )
        );

        clients.push(this._botClients[domain]);
    });

    clients = clients.filter((c) => Boolean(c));

    return Promise.all(
        clients.map(
            (client) => client.kill()
        )
    );
}

ClientPool.prototype.getOrCreateReconnectQueue = function(server) {
    if (server.getConcurrentReconnectLimit() === 0) {
        return null;
    }
    let q = this._reconnectQueues[server.domain];
    if (q === undefined) {
        q = this._reconnectQueues[server.domain] = new QueuePool(
            server.getConcurrentReconnectLimit(),
            (item) => {
                log.info(`Reconnecting client. ${q.waitingItems} left.`);
                return this._reconnectClient(item);
            }
        );
    }
    return q;
};

ClientPool.prototype.setBot = function(server, client) {
    this._botClients[server.domain] = client;
};

ClientPool.prototype.getBot = function(server) {
    return this._botClients[server.domain];
};

ClientPool.prototype.createIrcClient = function(ircClientConfig, matrixUser, isBot) {
    var bridgedClient = this._ircBridge.createBridgedClient(
        ircClientConfig, matrixUser, isBot
    );
    var server = bridgedClient.server;

    if (this._virtualClients[server.domain] === undefined) {
        this._virtualClients[server.domain] = {
            nicks: Object.create(null),
            userIds: Object.create(null)
        };
        this._virtualClientCounts[server.domain] = 0;
    }
    if (isBot) {
        this._botClients[server.domain] = bridgedClient;
    }

    // add event listeners
    bridgedClient.on("client-connected", this._onClientConnected.bind(this));
    bridgedClient.on("client-disconnected", this._onClientDisconnected.bind(this));
    bridgedClient.on("nick-change", this._onNickChange.bind(this));
    bridgedClient.on("join-error", this._onJoinError.bind(this));
    bridgedClient.on("irc-names", this._onNames.bind(this));

    // store the bridged client immediately in the pool even though it isn't
    // connected yet, else we could spawn 2 clients for a single user if this
    // function is called quickly.
    this._virtualClients[server.domain].userIds[bridgedClient.userId] = bridgedClient;
    this._virtualClientCounts[server.domain] = this._virtualClientCounts[server.domain] + 1;

    // Does this server have a max clients limit? If so, check if the limit is
    // reached and start cycling based on oldest time.
    this._checkClientLimit(server);
    return bridgedClient;
};

ClientPool.prototype.getBridgedClientByUserId = function(server, userId) {
    if (!this._virtualClients[server.domain]) {
        return undefined;
    }
    var cli = this._virtualClients[server.domain].userIds[userId];
    if (!cli || cli.isDead()) {
        return undefined;
    }
    return cli;
};

ClientPool.prototype.getBridgedClientByNick = function(server, nick) {
    var bot = this.getBot(server);
    if (bot && bot.nick === nick) {
        return bot;
    }

    if (!this._virtualClients[server.domain]) {
        return undefined;
    }
    var cli = this._virtualClients[server.domain].nicks[nick];
    if (!cli || cli.isDead()) {
        return undefined;
    }
    return cli;
};

ClientPool.prototype.getBridgedClientsForUserId = function(userId) {
    var domainList = Object.keys(this._virtualClients);
    var clientList = [];
    domainList.forEach((domain) => {
        var cli = this._virtualClients[domain].userIds[userId];
        if (cli && !cli.isDead()) {
            clientList.push(cli);
        }
    });
    return clientList;
};

ClientPool.prototype._checkClientLimit = function(server) {
    if (server.getMaxClients() === 0) {
        return;
    }

    var numConnections = this._getNumberOfConnections(server);
    this._sendConnectionMetric(server);

    if (numConnections < server.getMaxClients()) {
        // under the limit, we're good for now.
        log.debug(
            "%s active connections on %s",
            numConnections, server.domain
        );
        return;
    }

    log.debug(
        "%s active connections on %s (limit %s)",
        numConnections, server.domain, server.getMaxClients()
    );

    // find the oldest client to kill.
    var oldest = null;
    Object.keys(this._virtualClients[server.domain].nicks).forEach((nick) => {
        var client = this._virtualClients[server.domain].nicks[nick];
        if (!client) {
            // possible since undefined/null values can be present from culled entries
            return;
        }
        if (client.isBot) {
            return; // don't ever kick the bot off.
        }
        if (oldest === null) {
            oldest = client;
            return;
        }
        if (client.getLastActionTs() < oldest.getLastActionTs()) {
            oldest = client;
        }
    });
    if (!oldest) {
        return;
    }
    // disconnect and remove mappings.
    this._removeBridgedClient(oldest);
    oldest.disconnect("Client limit exceeded: " + server.getMaxClients()).then(
    function() {
        log.info("Client limit exceeded: Disconnected %s on %s.",
            oldest.nick, oldest.server.domain);
    },
    function(e) {
        log.error("Error when disconnecting %s on server %s: %s",
            oldest.nick, oldest.server.domain, JSON.stringify(e));
    });
};

ClientPool.prototype._getNumberOfConnections = function(server) {
    if (!server || !this._virtualClients[server.domain]) { return 0; }
    return this._virtualClientCounts[server.domain];
};

ClientPool.prototype.countTotalConnections = function() {
    var count = 0;

    Object.keys(this._virtualClients).forEach((domain) => {
        let server = this._ircBridge.getServer(domain);
        count += this._getNumberOfConnections(server);
    });

    return count;
};

ClientPool.prototype.totalReconnectsWaiting = function (serverDomain) {
    if (this._reconnectQueues[serverDomain] !== undefined) {
        return this._reconnectQueues[serverDomain].waitingItems;
    }
    return 0;
};

ClientPool.prototype.updateActiveConnectionMetrics = function(server, ageCounter) {
    if (this._virtualClients[server] === undefined) {
        return;
    }
    const clients = Object.values(this._virtualClients[server].userIds);
    clients.forEach((bridgedClient) => {
        if (!bridgedClient || bridgedClient.isDead()) {
            // We don't want to include dead ones, or ones that don't exist.
            return;
        }
        ageCounter.bump((Date.now() - bridgedClient.getLastActionTs()) / 1000);
    });
};

ClientPool.prototype.getNickUserIdMappingForChannel = function(server, channel) {
    const nickUserIdMap = {};
    const cliSet = this._virtualClients[server.domain].userIds;
    Object.keys(cliSet).filter((userId) =>
        cliSet[userId] && cliSet[userId].chanList
            && cliSet[userId].chanList.includes(channel)
    ).forEach((userId) => {
        nickUserIdMap[cliSet[userId].nick] = userId;
    });
    return nickUserIdMap;
};

ClientPool.prototype._sendConnectionMetric = function(server) {
    stats.ircClients(server.domain, this._getNumberOfConnections(server));
};

ClientPool.prototype._removeBridgedClient = function(bridgedClient) {
    var server = bridgedClient.server;
    this._virtualClients[server.domain].userIds[bridgedClient.userId] = undefined;
    this._virtualClients[server.domain].nicks[bridgedClient.nick] = undefined;
    this._virtualClientCounts[server.domain] = this._virtualClientCounts[server.domain] - 1;

    if (bridgedClient.isBot) {
        this._botClients[server.domain] = undefined;
    }
};

ClientPool.prototype._onClientConnected = function(bridgedClient) {
    var server = bridgedClient.server;
    var oldNick = bridgedClient.nick;
    var actualNick = bridgedClient.unsafeClient.nick;

    // assign a nick to this client
    this._virtualClients[server.domain].nicks[actualNick] = bridgedClient;

    // informative logging
    if (oldNick !== actualNick) {
        log.debug("Connected with nick '%s' instead of desired nick '%s'",
            actualNick, oldNick);
    }
};

ClientPool.prototype._onClientDisconnected = function(bridgedClient) {
    this._removeBridgedClient(bridgedClient);
    this._sendConnectionMetric(bridgedClient.server);

    if (bridgedClient.explicitDisconnect) {
        // don't reconnect users which explicitly disconnected e.g. client
        // cycling, idle timeouts, leaving rooms, etc.
        return;
    }
    // Reconnect this user
    // change the client config to use the current nick rather than the desired nick. This
    // makes sure that the client attempts to reconnect with the *SAME* nick, and also draws
    // from the latest !nick change, as the client config here may be very very old.
    var cliConfig = bridgedClient.getClientConfig();
    cliConfig.setDesiredNick(bridgedClient.nick);

    var cli = this.createIrcClient(
        cliConfig, bridgedClient.matrixUser, bridgedClient.isBot
    );
    var chanList = bridgedClient.chanList;
    // remove ref to the disconnected client so it can be GC'd. If we don't do this,
    // the timeout below holds it in a closure, preventing it from being GC'd.
    bridgedClient = undefined;

    if (chanList.length === 0) {
        log.info(`Dropping ${cli._id} ${cli.nick} because they are not joined to any channels`);
        return;
    }
    let queue = this.getOrCreateReconnectQueue(cli.server);
    if (queue === null) {
        this._reconnectClient({
            cli: cli,
            chanList: chanList,
        });
        return;
    }
    queue.enqueue(cli._id, {
        cli: cli,
        chanList: chanList,
    });
};

ClientPool.prototype._reconnectClient = function(cliChan) {
    const cli = cliChan.cli;
    const chanList = cliChan.chanList;
    return cli.connect().then(() => {
        log.info(
            "<%s> Reconnected %s@%s", cli._id, cli.nick, cli.server.domain
        );
        log.info("<%s> Rejoining %s channels", cli._id, chanList.length);
        chanList.forEach(function(c) {
            cli.joinChannel(c);
        });
        this._sendConnectionMetric(cli.server);
    }, (e) => {
        log.error(
            "<%s> Failed to reconnect %s@%s", cli._id, cli.nick, cli.server.domain
        );
    });
}

ClientPool.prototype._onNickChange = function(bridgedClient, oldNick, newNick) {
    this._virtualClients[bridgedClient.server.domain].nicks[oldNick] = undefined;
    this._virtualClients[bridgedClient.server.domain].nicks[newNick] = bridgedClient;
};

ClientPool.prototype._onJoinError = Promise.coroutine(function*(bridgedClient, chan, err) {
    var errorsThatShouldKick = [
        "err_bannedfromchan", // they aren't allowed in channels they are banned on.
        "err_inviteonlychan", // they aren't allowed in invite only channels
        "err_channelisfull", // they aren't allowed in if the channel is full
        "err_badchannelkey", // they aren't allowed in channels with a bad key
        "err_needreggednick", // they aren't allowed in +r channels if they haven't authed
    ];
    if (errorsThatShouldKick.indexOf(err) === -1) {
        return;
    }
    if (!bridgedClient.userId || bridgedClient.isBot) {
        return; // the bot itself can get these join errors
    }
    // TODO: this is a bit evil, no one in their right mind would expect
    // the client pool to be kicking matrix users from a room :(
    log.info(`Kicking ${bridgedClient.userId} from room due to ${err}`);
    let matrixRooms = yield this._ircBridge.getStore().getMatrixRoomsForChannel(
        bridgedClient.server, chan
    );
    let promises = matrixRooms.map((room) => {
        return this._ircBridge.getAppServiceBridge().getIntent().kick(
            room.getId(), bridgedClient.userId, `IRC error on ${chan}: ${err}`
        );
    });
    yield Promise.all(promises);
});

ClientPool.prototype._onNames = Promise.coroutine(function*(bridgedClient, chan, names) {
    let mls = this._ircBridge.memberListSyncers[bridgedClient.server.domain];
    if (!mls) {
        return;
    }
    yield mls.updateIrcMemberList(chan, names);
});

module.exports = ClientPool;
<commit_msg>Appease the linter<commit_after>/*eslint no-invalid-this: 0*/
/*
 * Maintains a lookup of connected IRC clients. These connections are transient
 * and may be closed for a variety of reasons.
 */
"use strict";
const stats = require("../config/stats");
const log = require("../logging").get("ClientPool");
const Promise = require("bluebird");
const QueuePool = require("../util/QueuePool");

function ClientPool(ircBridge) {
    this._ircBridge = ircBridge;
    // The list of bot clients on servers (not specific users)
    this._botClients = {
        // server_domain: BridgedClient
    };

    // list of virtual users on servers
    this._virtualClients = {
        // server_domain: {
        //    nicks: {
        //      <nickname>: BridgedClient
        //    },
        //    userIds: {
        //      <user_id>: BridgedClient
        //    }
        // }
    }
    // map of numbers of connected clients on each server
    // Counting these is quite expensive because we have to
    // ignore entries where the value is undefined. Instead,
    // just keep track of how many we have.
    this._virtualClientCounts = {
        // server_domain: number
    };

    this._reconnectQueues = {
        // server_domain: QueuePool
    };
}

ClientPool.prototype.killAllClients = function() {
    let domainList = Object.keys(this._virtualClients);
    let clients = [];
    domainList.forEach((domain) => {
        clients = clients.concat(
            Object.keys(this._virtualClients[domain].nicks).map(
                (nick) => this._virtualClients[domain].nicks[nick]
            )
        );

        clients = clients.concat(
            Object.keys(this._virtualClients[domain].userIds).map(
                (userId) => this._virtualClients[domain].userIds[userId]
            )
        );

        clients.push(this._botClients[domain]);
    });

    clients = clients.filter((c) => Boolean(c));

    return Promise.all(
        clients.map(
            (client) => client.kill()
        )
    );
}

ClientPool.prototype.getOrCreateReconnectQueue = function(server) {
    if (server.getConcurrentReconnectLimit() === 0) {
        return null;
    }
    let q = this._reconnectQueues[server.domain];
    if (q === undefined) {
        q = this._reconnectQueues[server.domain] = new QueuePool(
            server.getConcurrentReconnectLimit(),
            (item) => {
                log.info(`Reconnecting client. ${q.waitingItems} left.`);
                return this._reconnectClient(item);
            }
        );
    }
    return q;
};

ClientPool.prototype.setBot = function(server, client) {
    this._botClients[server.domain] = client;
};

ClientPool.prototype.getBot = function(server) {
    return this._botClients[server.domain];
};

ClientPool.prototype.createIrcClient = function(ircClientConfig, matrixUser, isBot) {
    var bridgedClient = this._ircBridge.createBridgedClient(
        ircClientConfig, matrixUser, isBot
    );
    var server = bridgedClient.server;

    if (this._virtualClients[server.domain] === undefined) {
        this._virtualClients[server.domain] = {
            nicks: Object.create(null),
            userIds: Object.create(null)
        };
        this._virtualClientCounts[server.domain] = 0;
    }
    if (isBot) {
        this._botClients[server.domain] = bridgedClient;
    }

    // add event listeners
    bridgedClient.on("client-connected", this._onClientConnected.bind(this));
    bridgedClient.on("client-disconnected", this._onClientDisconnected.bind(this));
    bridgedClient.on("nick-change", this._onNickChange.bind(this));
    bridgedClient.on("join-error", this._onJoinError.bind(this));
    bridgedClient.on("irc-names", this._onNames.bind(this));

    // store the bridged client immediately in the pool even though it isn't
    // connected yet, else we could spawn 2 clients for a single user if this
    // function is called quickly.
    this._virtualClients[server.domain].userIds[bridgedClient.userId] = bridgedClient;
    this._virtualClientCounts[server.domain] = this._virtualClientCounts[server.domain] + 1;

    // Does this server have a max clients limit? If so, check if the limit is
    // reached and start cycling based on oldest time.
    this._checkClientLimit(server);
    return bridgedClient;
};

ClientPool.prototype.getBridgedClientByUserId = function(server, userId) {
    if (!this._virtualClients[server.domain]) {
        return undefined;
    }
    var cli = this._virtualClients[server.domain].userIds[userId];
    if (!cli || cli.isDead()) {
        return undefined;
    }
    return cli;
};

ClientPool.prototype.getBridgedClientByNick = function(server, nick) {
    var bot = this.getBot(server);
    if (bot && bot.nick === nick) {
        return bot;
    }

    if (!this._virtualClients[server.domain]) {
        return undefined;
    }
    var cli = this._virtualClients[server.domain].nicks[nick];
    if (!cli || cli.isDead()) {
        return undefined;
    }
    return cli;
};

ClientPool.prototype.getBridgedClientsForUserId = function(userId) {
    var domainList = Object.keys(this._virtualClients);
    var clientList = [];
    domainList.forEach((domain) => {
        var cli = this._virtualClients[domain].userIds[userId];
        if (cli && !cli.isDead()) {
            clientList.push(cli);
        }
    });
    return clientList;
};

ClientPool.prototype._checkClientLimit = function(server) {
    if (server.getMaxClients() === 0) {
        return;
    }

    var numConnections = this._getNumberOfConnections(server);
    this._sendConnectionMetric(server);

    if (numConnections < server.getMaxClients()) {
        // under the limit, we're good for now.
        log.debug(
            "%s active connections on %s",
            numConnections, server.domain
        );
        return;
    }

    log.debug(
        "%s active connections on %s (limit %s)",
        numConnections, server.domain, server.getMaxClients()
    );

    // find the oldest client to kill.
    var oldest = null;
    Object.keys(this._virtualClients[server.domain].nicks).forEach((nick) => {
        var client = this._virtualClients[server.domain].nicks[nick];
        if (!client) {
            // possible since undefined/null values can be present from culled entries
            return;
        }
        if (client.isBot) {
            return; // don't ever kick the bot off.
        }
        if (oldest === null) {
            oldest = client;
            return;
        }
        if (client.getLastActionTs() < oldest.getLastActionTs()) {
            oldest = client;
        }
    });
    if (!oldest) {
        return;
    }
    // disconnect and remove mappings.
    this._removeBridgedClient(oldest);
    oldest.disconnect("Client limit exceeded: " + server.getMaxClients()).then(
    function() {
        log.info("Client limit exceeded: Disconnected %s on %s.",
            oldest.nick, oldest.server.domain);
    },
    function(e) {
        log.error("Error when disconnecting %s on server %s: %s",
            oldest.nick, oldest.server.domain, JSON.stringify(e));
    });
};

ClientPool.prototype._getNumberOfConnections = function(server) {
    if (!server || !this._virtualClients[server.domain]) { return 0; }
    return this._virtualClientCounts[server.domain];
};

ClientPool.prototype.countTotalConnections = function() {
    var count = 0;

    Object.keys(this._virtualClients).forEach((domain) => {
        let server = this._ircBridge.getServer(domain);
        count += this._getNumberOfConnections(server);
    });

    return count;
};

ClientPool.prototype.totalReconnectsWaiting = function (serverDomain) {
    if (this._reconnectQueues[serverDomain] !== undefined) {
        return this._reconnectQueues[serverDomain].waitingItems;
    }
    return 0;
};

ClientPool.prototype.updateActiveConnectionMetrics = function(server, ageCounter) {
    if (this._virtualClients[server] === undefined) {
        return;
    }
    const clients = Object.values(this._virtualClients[server].userIds);
    clients.forEach((bridgedClient) => {
        if (!bridgedClient || bridgedClient.isDead()) {
            // We don't want to include dead ones, or ones that don't exist.
            return;
        }
        ageCounter.bump((Date.now() - bridgedClient.getLastActionTs()) / 1000);
    });
};

ClientPool.prototype.getNickUserIdMappingForChannel = function(server, channel) {
    const nickUserIdMap = {};
    const cliSet = this._virtualClients[server.domain].userIds;
    Object.keys(cliSet).filter((userId) =>
        cliSet[userId] && cliSet[userId].chanList
            && cliSet[userId].chanList.includes(channel)
    ).forEach((userId) => {
        nickUserIdMap[cliSet[userId].nick] = userId;
    });
    return nickUserIdMap;
};

ClientPool.prototype._sendConnectionMetric = function(server) {
    stats.ircClients(server.domain, this._getNumberOfConnections(server));
};

ClientPool.prototype._removeBridgedClient = function(bridgedClient) {
    var server = bridgedClient.server;
    this._virtualClients[server.domain].userIds[bridgedClient.userId] = undefined;
    this._virtualClients[server.domain].nicks[bridgedClient.nick] = undefined;
    this._virtualClientCounts[server.domain] = this._virtualClientCounts[server.domain] - 1;

    if (bridgedClient.isBot) {
        this._botClients[server.domain] = undefined;
    }
};

ClientPool.prototype._onClientConnected = function(bridgedClient) {
    var server = bridgedClient.server;
    var oldNick = bridgedClient.nick;
    var actualNick = bridgedClient.unsafeClient.nick;

    // assign a nick to this client
    this._virtualClients[server.domain].nicks[actualNick] = bridgedClient;

    // informative logging
    if (oldNick !== actualNick) {
        log.debug("Connected with nick '%s' instead of desired nick '%s'",
            actualNick, oldNick);
    }
};

ClientPool.prototype._onClientDisconnected = function(bridgedClient) {
    this._removeBridgedClient(bridgedClient);
    this._sendConnectionMetric(bridgedClient.server);

    if (bridgedClient.explicitDisconnect) {
        // don't reconnect users which explicitly disconnected e.g. client
        // cycling, idle timeouts, leaving rooms, etc.
        return;
    }
    // Reconnect this user
    // change the client config to use the current nick rather than the desired nick. This
    // makes sure that the client attempts to reconnect with the *SAME* nick, and also draws
    // from the latest !nick change, as the client config here may be very very old.
    var cliConfig = bridgedClient.getClientConfig();
    cliConfig.setDesiredNick(bridgedClient.nick);

    var cli = this.createIrcClient(
        cliConfig, bridgedClient.matrixUser, bridgedClient.isBot
    );
    var chanList = bridgedClient.chanList;
    // remove ref to the disconnected client so it can be GC'd. If we don't do this,
    // the timeout below holds it in a closure, preventing it from being GC'd.
    bridgedClient = undefined;

    if (chanList.length === 0) {
        log.info(`Dropping ${cli._id} ${cli.nick} because they are not joined to any channels`);
        return;
    }
    let queue = this.getOrCreateReconnectQueue(cli.server);
    if (queue === null) {
        this._reconnectClient({
            cli: cli,
            chanList: chanList,
        });
        return;
    }
    queue.enqueue(cli._id, {
        cli: cli,
        chanList: chanList,
    });
};

ClientPool.prototype._reconnectClient = function(cliChan) {
    const cli = cliChan.cli;
    const chanList = cliChan.chanList;
    return cli.connect().then(() => {
        log.info(
            "<%s> Reconnected %s@%s", cli._id, cli.nick, cli.server.domain
        );
        log.info("<%s> Rejoining %s channels", cli._id, chanList.length);
        chanList.forEach(function(c) {
            cli.joinChannel(c);
        });
        this._sendConnectionMetric(cli.server);
    }, (e) => {
        log.error(
            "<%s> Failed to reconnect %s@%s", cli._id, cli.nick, cli.server.domain
        );
    });
}

ClientPool.prototype._onNickChange = function(bridgedClient, oldNick, newNick) {
    this._virtualClients[bridgedClient.server.domain].nicks[oldNick] = undefined;
    this._virtualClients[bridgedClient.server.domain].nicks[newNick] = bridgedClient;
};

ClientPool.prototype._onJoinError = Promise.coroutine(function*(bridgedClient, chan, err) {
    var errorsThatShouldKick = [
        "err_bannedfromchan", // they aren't allowed in channels they are banned on.
        "err_inviteonlychan", // they aren't allowed in invite only channels
        "err_channelisfull", // they aren't allowed in if the channel is full
        "err_badchannelkey", // they aren't allowed in channels with a bad key
        "err_needreggednick", // they aren't allowed in +r channels if they haven't authed
    ];
    if (errorsThatShouldKick.indexOf(err) === -1) {
        return;
    }
    if (!bridgedClient.userId || bridgedClient.isBot) {
        return; // the bot itself can get these join errors
    }
    // TODO: this is a bit evil, no one in their right mind would expect
    // the client pool to be kicking matrix users from a room :(
    log.info(`Kicking ${bridgedClient.userId} from room due to ${err}`);
    let matrixRooms = yield this._ircBridge.getStore().getMatrixRoomsForChannel(
        bridgedClient.server, chan
    );
    let promises = matrixRooms.map((room) => {
        return this._ircBridge.getAppServiceBridge().getIntent().kick(
            room.getId(), bridgedClient.userId, `IRC error on ${chan}: ${err}`
        );
    });
    yield Promise.all(promises);
});

ClientPool.prototype._onNames = Promise.coroutine(function*(bridgedClient, chan, names) {
    let mls = this._ircBridge.memberListSyncers[bridgedClient.server.domain];
    if (!mls) {
        return;
    }
    yield mls.updateIrcMemberList(chan, names);
});

module.exports = ClientPool;
<|endoftext|>