<commit_before>var util = require('./util.js');
var parseTime = util.parseTime;
var legend = util.legend;
var spawn = require('child_process').spawn;

function Graph(rrd) {
  this._rrd = rrd;
  this.lines = [];
  this.cdefs = [];
}

Graph.prototype.start = Graph.prototype.from = function(time) {
  this._start = time;
  this._interval = parseTime(time);
  return this;
}
Graph.prototype.end = Graph.prototype.to = function(time) {
  this._end = time;
  return this;
}
Graph.prototype.smooth = function(name, factor) {
  var self = this;
  factor = typeof factor == 'string' ? parseTime(factor) : factor;
  return {
    as: function(newName) {
      self.cdefs.push('CDEF:'+ newName + '=' + name + ',' + factor + ',TREND');
      return self;
    }
  };
}
Graph.prototype.table = function() {
  this._drawTable = true;
  return this;
}

Graph.prototype.title = function(title) {
  this._title = title;
  return this;
}
Graph.prototype.line = function(name, color) {
  this.lines.push('LINE:' + name + color + ':' + legend(name));
  return this;
}
Graph.prototype.slope = function(slope) {
  this._slope = slope === undefined ? true : slope;
  return this;
}
Graph.prototype.watermark = function(str) {
  this._watermark = str;
  return this;
}
Graph.prototype.vlabel = function(str) {
  this._vlabel = str;
  return this;
}
Graph.prototype.create = function(file) {
  var defs = this._rrd._dataSources.map(function(ds) {
    return 'DEF:' + ds.name + '=' + this._rrd._file + ':' + ds.name + ':AVERAGE';
  }, this);
  var cdefs = this.cdefs.map(function(cdef) {
    return cdef;
  });
  console.log(defs);
  var params = [
    'graph', file,
    '--start', this._start];
  this._slope && params.push('--slope-mode');
  if(this._watermark) {
    params.push('--watermark');
    params.push(this._watermark);
  }
  if(this._title) {
    params.push('--title');
    params.push(this._title);
  }
  if(this._vlabel) {
    params.push('--vertical-label');
    params.push(this._vlabel);
  }
  params.push('--font');
  params.push('DEFAULT:8:Anonymous Pro');
  var lines = this.lines;
  if(this._drawTable) {
    params.push('COMMENT:           ');
    params.push('COMMENT:        Cur');
    params.push('COMMENT:        Avg');
    params.push('COMMENT:        Max');
    params.push('COMMENT:        Min\\l');
    lines = lines.reduce(function(lines, line) {
      console.log(line);
      var df = line.match(/^.*:(.*?)#/)[1];
      cdefs.push('VDEF:' + df + '_cur=' + df +',LAST');
      cdefs.push('VDEF:' + df + '_avg=' + df +',AVERAGE');
      cdefs.push('VDEF:' + df + '_max=' + df +',MAXIMUM');
      cdefs.push('VDEF:' + df + '_min=' + df +',MINIMUM');
      lines.push(line);
      lines.push('GPRINT:' + df + '_cur:%10.2lf%S');
      lines.push('GPRINT:' + df + '_avg:%10.2lf%S');
      lines.push('GPRINT:' + df + '_max:%10.2lf%S');
      lines.push('GPRINT:' + df + '_min:%10.2lf%S\\l');
      return lines;
    }, [])
  }
  params = params
    .concat(defs)
    .concat(cdefs)
    .concat(lines)
  var rrd = spawn('rrdtool', params);
  console.log(params);
  rrd.stderr.pipe(process.stderr);
}

module.exports = Graph;
<commit_msg>Proper `private` variable names<commit_after>var util = require('./util.js');
var parseTime = util.parseTime;
var legend = util.legend;
var spawn = require('child_process').spawn;

function Graph(rrd) {
  this._rrd = rrd;
  this._lines = [];
  this._cdefs = [];
}

Graph.prototype.start = Graph.prototype.from = function(time) {
  this._start = time;
  this._interval = parseTime(time);
  return this;
}
Graph.prototype.end = Graph.prototype.to = function(time) {
  this._end = time;
  return this;
}
Graph.prototype.smooth = function(name, factor) {
  var self = this;
  factor = typeof factor == 'string' ? parseTime(factor) : factor;
  return {
    as: function(newName) {
      self._cdefs.push('CDEF:'+ newName + '=' + name + ',' + factor + ',TREND');
      return self;
    }
  };
}
Graph.prototype.table = function() {
  this._drawTable = true;
  return this;
}

Graph.prototype.title = function(title) {
  this._title = title;
  return this;
}
Graph.prototype.line = function(name, color) {
  this._lines.push('LINE:' + name + color + ':' + legend(name));
  return this;
}
Graph.prototype.slope = function(slope) {
  this._slope = slope === undefined ? true : slope;
  return this;
}
Graph.prototype.watermark = function(str) {
  this._watermark = str;
  return this;
}
Graph.prototype.vlabel = function(str) {
  this._vlabel = str;
  return this;
}
Graph.prototype.create = function(file) {
  var defs = this._rrd._dataSources.map(function(ds) {
    return 'DEF:' + ds.name + '=' + this._rrd._file + ':' + ds.name + ':AVERAGE';
  }, this);
  var cdefs = this._cdefs.map(function(cdef) {
    return cdef;
  });
  console.log(defs);
  var params = [
    'graph', file,
    '--start', this._start];
  this._slope && params.push('--slope-mode');
  if(this._watermark) {
    params.push('--watermark');
    params.push(this._watermark);
  }
  if(this._title) {
    params.push('--title');
    params.push(this._title);
  }
  if(this._vlabel) {
    params.push('--vertical-label');
    params.push(this._vlabel);
  }
  var lines = this._lines;
  if(this._drawTable) {
    params.push('COMMENT:           ');
    params.push('COMMENT:        Cur');
    params.push('COMMENT:        Avg');
    params.push('COMMENT:        Max');
    params.push('COMMENT:        Min\\l');
    lines = lines.reduce(function(lines, line) {
      console.log(line);
      var df = line.match(/^.*:(.*?)#/)[1];
      cdefs.push('VDEF:' + df + '_cur=' + df +',LAST');
      cdefs.push('VDEF:' + df + '_avg=' + df +',AVERAGE');
      cdefs.push('VDEF:' + df + '_max=' + df +',MAXIMUM');
      cdefs.push('VDEF:' + df + '_min=' + df +',MINIMUM');
      lines.push(line);
      lines.push('GPRINT:' + df + '_cur:%7.2lf%Sbps');
      lines.push('GPRINT:' + df + '_avg:%10.2lf%S');
      lines.push('GPRINT:' + df + '_max:%10.2lf%S');
      lines.push('GPRINT:' + df + '_min:%10.2lf%S\\l');
      return lines;
    }, [])
  }
  params = params
    .concat(defs)
    .concat(cdefs)
    .concat(lines)
  var rrd = spawn('rrdtool', params);
  console.log(params);
  rrd.stderr.pipe(process.stderr);
}

module.exports = Graph;
<|endoftext|>


<commit_before>'use strict';

const Helpers = require('../../helpers');

var _ = {
    intersection: require('lodash/intersection'),
    difference: require('lodash/difference'),
    each: require('lodash/each'),
    uniq: require('lodash/uniq'),
};

var handlers = {
	RPL_WELCOME: function(command) {
        var nick =  command.params[0];

        // Get the server name so we know which messages are by the server in future
        this.network.server = command.prefix;

        this.network.cap.negotiating = false;
        this.emit('registered', {
            nick: nick
        });
    },

    RPL_ISUPPORT: function(command) {
        var options = command.params;
        var i;
        var option;
        var matches;
        var j;

        for (i = 1; i < options.length; i++) {
            option = options[i].split('=', 2);
            option[0] = option[0].toUpperCase();

            this.network.options[option[0]] = (typeof option[1] !== 'undefined') ? option[1] : true;

            if (option[0] === 'PREFIX') {
                matches = /\(([^)]*)\)(.*)/.exec(option[1]);
                if (matches && matches.length === 3) {
                    this.network.options.PREFIX = [];
                    for (j = 0; j < matches[2].length; j++) {
                        this.network.options.PREFIX.push({
                            symbol: matches[2].charAt(j),
                            mode: matches[1].charAt(j)
                        });
                    }
                }
            } else if (option[0] === 'CHANTYPES') {
                this.network.options.CHANTYPES = this.network.options.CHANTYPES.split('');
            } else if (option[0] === 'CHANMODES') {
                this.network.options.CHANMODES = option[1].split(',');
            } else if (option[0] === 'NETWORK') {
                this.network.name = option[1];
            } else if (option[0] === 'NAMESX' && !this.network.cap.isEnabled('multi-prefix')) {
                // Tell the server to send us all user modes in NAMES reply, not just
                // the highest one
                this.connection.write('PROTOCTL NAMESX');
            }
        }

        this.emit('server options', {
            options: this.network.options,
            cap: this.network.cap.enabled
        });
    },

    CAP: function(command) {
        var request_caps = [];
        var capability_values = Object.create(null);

        // TODO: capability modifiers
        // i.e. - for disable, ~ for requires ACK, = for sticky
        var capabilities = command.params[command.params.length - 1]
            .replace(/(?:^| )[\-~=]/, '')
            .split(' ')
            .map(function(cap) {
                // CAPs in 3.2 may be in the form of CAP=VAL. So seperate those out
                var sep = cap.indexOf('=');
                if (sep === -1) {
                    return cap;
                }

                var cap_name = cap.substr(0, sep);
                var cap_value = cap.substr(sep + 1);

                capability_values[cap_name] = cap_value;
                return cap_name;
            });

        // Which capabilities we want to enable
        var want = [
            'batch',
            'multi-prefix',
            'draft/message-tags-0.2',
            'away-notify',
            'invite-notify',
            'account-notify',
            'account-tag',
            'server-time',
            'userhost-in-names',
            'extended-join',
            'znc.in/server-time-iso',
            'znc.in/server-time'
        ];

        // Optional CAPs depending on settings
        if (this.connection.options.password) {
            want.push('sasl');
        }
        if (this.connection.options.enable_chghost) {
            want.push('chghost');
        }
        if (this.connection.options.enable_echomessage) {
            want.push('echo-message');
        }

        want = _.uniq(want.concat(this.request_extra_caps));

        switch (command.params[1]) {
            case 'LS':
                // Compute which of the available capabilities we want and request them
                request_caps = _.intersection(capabilities, want);
                if (request_caps.length > 0) {
                    this.network.cap.requested = this.network.cap.requested.concat(request_caps);
                }

                // CAP 3.2 multline support. Only send our CAP requests on the last CAP LS
                // line which will not have * set for params[2]
                if (command.params[2] !== '*') {
                    if (request_caps.length > 0) {
                        this.connection.write('CAP REQ :' + request_caps.join(' '));
                    } else {
                        this.connection.write('CAP END');
                        this.network.cap.negotiating = false;
                    }
                }
                break;
            case 'ACK':
                if (capabilities.length > 0) {
                    // Update list of enabled capabilities
                    this.network.cap.enabled = capabilities;
                    // Update list of capabilities we would like to have but that aren't enabled
                    this.network.cap.requested = _.difference(
                        this.network.cap.requested,
                        capabilities
                    );
                }
                if (this.network.cap.enabled.length > 0) {
                    if (this.network.cap.isEnabled('sasl')) {
                        if (this.connection.options.sasl_mechanism === 'AUTHCOOKIE') {
                            this.connection.write('AUTHENTICATE AUTHCOOKIE');
                        } else {
                            this.connection.write('AUTHENTICATE PLAIN');
                        }
                    } else {
                        this.connection.write('CAP END');
                        this.network.cap.negotiating = false;
                    }
                }
                break;
            case 'NAK':
                if (capabilities.length > 0) {
                    this.network.cap.requested = _.difference(
                        this.network.cap.requested,
                        capabilities
                    );
                }
                if (this.network.cap.requested.length > 0) {
                    this.connection.write('CAP END');
                    this.network.cap.negotiating = false;
                }
                break;
            case 'LIST':
                // should we do anything here?
                break;
            case 'NEW':
                // Not supported yet
                break;
            case 'DEL':
                // Not supported yet
                break;
        }
    },

    AUTHENTICATE: function(command) {
        var auth_str = this.connection.options.nick + '\0' +
            this.connection.options.nick + '\0' +
            this.connection.options.password;
        var b = new Buffer(auth_str, 'utf8');
        var b64 = b.toString('base64');

        if (command.params[0] === '+') {
            while (b64.length >= 400) {
                this.connection.write('AUTHENTICATE ' + b64.slice(0, 399));
                b64 = b64.slice(399);
            }
            if (b64.length > 0) {
                this.connection.write('AUTHENTICATE ' + b64);
            } else {
                this.connection.write('AUTHENTICATE +');
            }
        } else {
            this.connection.write('CAP END');
            this.network.cap.negotiating = false;
        }
    },

    RPL_LOGGEDIN: function(command) {
        if (this.network.cap.negotiating === true) {
            this.connection.write('CAP END');
            this.network.cap.negotiating = false;
        }

        var mask = Helpers.parseMask(command.params[1]);

        // Check if we have a server-time
        var time = command.getServerTime();

        this.emit('loggedin', {
            nick: command.params[0],
            ident: mask.user,
            hostname: mask.host,
            account: command.params[2],
            time: time
        });

        this.emit('account', {
            nick: command.params[0],
            ident: mask.user,
            hostname: mask.host,
            account: command.params[2],
            time: time
        });
    },

    RPL_LOGGEDOUT: function(command) {
        var mask = Helpers.parseMask(command.params[1]);

        // Check if we have a server-time
        var time = command.getServerTime();

        this.emit('loggedout', {
            nick: command.params[0],
            ident: mask.user,
            hostname: mask.host,
            account: '',
            time: time
        });

        this.emit('account', {
            nick: command.params[0],
            ident: mask.user,
            hostname: mask.host,
            account: '',
            time: time
        });
    },

    RPL_SASLLOGGEDIN: function() {
        if (this.network.cap.negotiating === true) {
            this.connection.write('CAP END');
            this.network.cap.negotiating = false;
        }
    },

    ERR_SASLNOTAUTHORISED: function() {
        this.connection.write('CAP END');
        this.network.cap.negotiating = false;
    },

    ERR_SASLABORTED: function() {
        this.connection.write('CAP END');
        this.network.cap.negotiating = false;
    },

    ERR_SASLALREADYAUTHED: function() {
        // noop
    }
};

module.exports = function AddCommandHandlers(command_controller) {
    _.each(handlers, function(handler, handler_command) {
        command_controller.addHandler(handler_command, handler);
    });
};
<commit_msg>Send account: false for loggedout messages<commit_after>'use strict';

const Helpers = require('../../helpers');

var _ = {
    intersection: require('lodash/intersection'),
    difference: require('lodash/difference'),
    each: require('lodash/each'),
    uniq: require('lodash/uniq'),
};

var handlers = {
	RPL_WELCOME: function(command) {
        var nick =  command.params[0];

        // Get the server name so we know which messages are by the server in future
        this.network.server = command.prefix;

        this.network.cap.negotiating = false;
        this.emit('registered', {
            nick: nick
        });
    },

    RPL_ISUPPORT: function(command) {
        var options = command.params;
        var i;
        var option;
        var matches;
        var j;

        for (i = 1; i < options.length; i++) {
            option = options[i].split('=', 2);
            option[0] = option[0].toUpperCase();

            this.network.options[option[0]] = (typeof option[1] !== 'undefined') ? option[1] : true;

            if (option[0] === 'PREFIX') {
                matches = /\(([^)]*)\)(.*)/.exec(option[1]);
                if (matches && matches.length === 3) {
                    this.network.options.PREFIX = [];
                    for (j = 0; j < matches[2].length; j++) {
                        this.network.options.PREFIX.push({
                            symbol: matches[2].charAt(j),
                            mode: matches[1].charAt(j)
                        });
                    }
                }
            } else if (option[0] === 'CHANTYPES') {
                this.network.options.CHANTYPES = this.network.options.CHANTYPES.split('');
            } else if (option[0] === 'CHANMODES') {
                this.network.options.CHANMODES = option[1].split(',');
            } else if (option[0] === 'NETWORK') {
                this.network.name = option[1];
            } else if (option[0] === 'NAMESX' && !this.network.cap.isEnabled('multi-prefix')) {
                // Tell the server to send us all user modes in NAMES reply, not just
                // the highest one
                this.connection.write('PROTOCTL NAMESX');
            }
        }

        this.emit('server options', {
            options: this.network.options,
            cap: this.network.cap.enabled
        });
    },

    CAP: function(command) {
        var request_caps = [];
        var capability_values = Object.create(null);

        // TODO: capability modifiers
        // i.e. - for disable, ~ for requires ACK, = for sticky
        var capabilities = command.params[command.params.length - 1]
            .replace(/(?:^| )[\-~=]/, '')
            .split(' ')
            .map(function(cap) {
                // CAPs in 3.2 may be in the form of CAP=VAL. So seperate those out
                var sep = cap.indexOf('=');
                if (sep === -1) {
                    return cap;
                }

                var cap_name = cap.substr(0, sep);
                var cap_value = cap.substr(sep + 1);

                capability_values[cap_name] = cap_value;
                return cap_name;
            });

        // Which capabilities we want to enable
        var want = [
            'batch',
            'multi-prefix',
            'draft/message-tags-0.2',
            'away-notify',
            'invite-notify',
            'account-notify',
            'account-tag',
            'server-time',
            'userhost-in-names',
            'extended-join',
            'znc.in/server-time-iso',
            'znc.in/server-time'
        ];

        // Optional CAPs depending on settings
        if (this.connection.options.password) {
            want.push('sasl');
        }
        if (this.connection.options.enable_chghost) {
            want.push('chghost');
        }
        if (this.connection.options.enable_echomessage) {
            want.push('echo-message');
        }

        want = _.uniq(want.concat(this.request_extra_caps));

        switch (command.params[1]) {
            case 'LS':
                // Compute which of the available capabilities we want and request them
                request_caps = _.intersection(capabilities, want);
                if (request_caps.length > 0) {
                    this.network.cap.requested = this.network.cap.requested.concat(request_caps);
                }

                // CAP 3.2 multline support. Only send our CAP requests on the last CAP LS
                // line which will not have * set for params[2]
                if (command.params[2] !== '*') {
                    if (request_caps.length > 0) {
                        this.connection.write('CAP REQ :' + request_caps.join(' '));
                    } else {
                        this.connection.write('CAP END');
                        this.network.cap.negotiating = false;
                    }
                }
                break;
            case 'ACK':
                if (capabilities.length > 0) {
                    // Update list of enabled capabilities
                    this.network.cap.enabled = capabilities;
                    // Update list of capabilities we would like to have but that aren't enabled
                    this.network.cap.requested = _.difference(
                        this.network.cap.requested,
                        capabilities
                    );
                }
                if (this.network.cap.enabled.length > 0) {
                    if (this.network.cap.isEnabled('sasl')) {
                        if (this.connection.options.sasl_mechanism === 'AUTHCOOKIE') {
                            this.connection.write('AUTHENTICATE AUTHCOOKIE');
                        } else {
                            this.connection.write('AUTHENTICATE PLAIN');
                        }
                    } else {
                        this.connection.write('CAP END');
                        this.network.cap.negotiating = false;
                    }
                }
                break;
            case 'NAK':
                if (capabilities.length > 0) {
                    this.network.cap.requested = _.difference(
                        this.network.cap.requested,
                        capabilities
                    );
                }
                if (this.network.cap.requested.length > 0) {
                    this.connection.write('CAP END');
                    this.network.cap.negotiating = false;
                }
                break;
            case 'LIST':
                // should we do anything here?
                break;
            case 'NEW':
                // Not supported yet
                break;
            case 'DEL':
                // Not supported yet
                break;
        }
    },

    AUTHENTICATE: function(command) {
        var auth_str = this.connection.options.nick + '\0' +
            this.connection.options.nick + '\0' +
            this.connection.options.password;
        var b = new Buffer(auth_str, 'utf8');
        var b64 = b.toString('base64');

        if (command.params[0] === '+') {
            while (b64.length >= 400) {
                this.connection.write('AUTHENTICATE ' + b64.slice(0, 399));
                b64 = b64.slice(399);
            }
            if (b64.length > 0) {
                this.connection.write('AUTHENTICATE ' + b64);
            } else {
                this.connection.write('AUTHENTICATE +');
            }
        } else {
            this.connection.write('CAP END');
            this.network.cap.negotiating = false;
        }
    },

    RPL_LOGGEDIN: function(command) {
        if (this.network.cap.negotiating === true) {
            this.connection.write('CAP END');
            this.network.cap.negotiating = false;
        }

        var mask = Helpers.parseMask(command.params[1]);

        // Check if we have a server-time
        var time = command.getServerTime();

        this.emit('loggedin', {
            nick: command.params[0],
            ident: mask.user,
            hostname: mask.host,
            account: command.params[2],
            time: time
        });

        this.emit('account', {
            nick: command.params[0],
            ident: mask.user,
            hostname: mask.host,
            account: command.params[2],
            time: time
        });
    },

    RPL_LOGGEDOUT: function(command) {
        var mask = Helpers.parseMask(command.params[1]);

        // Check if we have a server-time
        var time = command.getServerTime();

        this.emit('loggedout', {
            nick: command.params[0],
            ident: mask.user,
            hostname: mask.host,
            account: false,
            time: time
        });

        this.emit('account', {
            nick: command.params[0],
            ident: mask.user,
            hostname: mask.host,
            account: false,
            time: time
        });
    },

    RPL_SASLLOGGEDIN: function() {
        if (this.network.cap.negotiating === true) {
            this.connection.write('CAP END');
            this.network.cap.negotiating = false;
        }
    },

    ERR_SASLNOTAUTHORISED: function() {
        this.connection.write('CAP END');
        this.network.cap.negotiating = false;
    },

    ERR_SASLABORTED: function() {
        this.connection.write('CAP END');
        this.network.cap.negotiating = false;
    },

    ERR_SASLALREADYAUTHED: function() {
        // noop
    }
};

module.exports = function AddCommandHandlers(command_controller) {
    _.each(handlers, function(handler, handler_command) {
        command_controller.addHandler(handler_command, handler);
    });
};
<|endoftext|>


<commit_before>/*
Class: CollapsingNav

Monitors the padding of nav elements inside a flexible horizontal nav container, putting items into a submenu when the padding gets too small (ie items get scrunched together).  Kinda limited in scope, in that it won't work for items with fixed padding, but that could be added as a different calculation method.

Example Usage:
	$sectionNavs = jQuery('.sectionNav');
	if($sectionNavs.length){
		$sectionNavs.each(function(){
			var $this = jQuery(this);
			var _collapsingNav = new __CollapsingNav({
				$: $this
				,doHandleResize: function(){
					return this.$.is('.current');
				}
			});
			var _dropDownMenu = new __.classes.DropDownMenu({
				elmWrapper: $this
				,itemSelector: '> .topItem'
			});
		});
	}

*/
/* global clearTimeout, define, setTimeout, window */
define(['tmclasses/tmclasses', 'jquery', './DropDown'], function(__tmclasses, jQuery, __DropDown){
	var __hasTooManyItemsMethods = {
		itemPadding: function(){
			var _this = this;
			var $navItems = this.mainList.find(this.navItemSelector);
			var _hasEnoughHeightDifference = false;
			var _hasTooLittlePadding = false;

			$navItems.each(function(){
				var $this = jQuery(this);
				var $topLevel = $this.find(_this.topLevelSelector);
				var _itemPadding = $this.outerWidth() - $topLevel.outerWidth();
				if(_itemPadding < _this.minimumPadding){
					_hasTooLittlePadding = true;
					return false;
				}
				//--check height to see if item wraps
				//-# must do this for each item, since individual items may have different font-size, etc
				var _html = $topLevel.html();
				$topLevel.html('x');
				var _singleLineHeight = $topLevel.outerHeight();
				$topLevel.html(_html);
				var _outerHeight = $topLevel.outerHeight();
				if(
					_singleLineHeight
					&& Math.abs(_outerHeight - _singleLineHeight) > 0.3 * _singleLineHeight
				){
					_hasEnoughHeightDifference = true;
					return false;
				}
			});
			return _hasEnoughHeightDifference || _hasTooLittlePadding;
		}
		,rightPos: function(){
			var _this = this;
			var $container = _this.getContainer();

			var containerRight = $container.offset().left + $container.outerWidth();
			var mainListRight = _this.mainList.offset().left + _this.mainList.outerWidth();

			return mainListRight > containerRight;
		}
	};

	var __CollapsingNav = __tmclasses.create({
		init: function(){
			this.__parent(arguments);
			var _this = this;

			if(!_this.moreItem){
				_this.moreItem = jQuery(_this.moreItemHTML);
			}
			_this.moreItem.data('isAttached', false);

			if(!_this.moreList){
				_this.moreList = _this.moreItem.find(_this.moreListSelector);
			}

			if(!_this.mainList){
				_this.mainList = _this.$.find(_this.mainListSelector);
			}

			if(_this.dropDown === undefined){
				_this.dropDown = new __DropDown(_this.getDropDownOpts());
			}

			if(_this.isActive){
				_this.activate();
			}
		}
		,properties: {
			$: null
			,$window: jQuery(window)
			,activate: function(){
				//--attach resize listener
				this.$window.on('resize', jQuery.proxy(this.handleResizeInInterval, this));

				this.isActive = true;

				this.handleResize();
			}
			,container: function(){
				this.container = jQuery('body');
				return this.container;
			}
			,dropDown: undefined
			,getContainer: function(){
				if(typeof this.container === 'function'){
					return this.container();
				}else{
					return this.container;
				}
			}
			,getDropDownOpts: function(_opts){
				return jQuery.extend({
					$: this.moreItem
					,itemSelector: 'this'
				}, _opts);
			}
			,deactivate: function(){
				clearTimeout(this.resizeTimeout);

				//--detach resize listener
				this.$window.off('resize', jQuery.proxy(this.handleResizeInInterval, this));

				if(this.dropDown){
					this.dropDown.deactivate();
				}

				this.isActive = false;

				this.popAllFromMoreList();
			}
			,doHandleResize: true
			,fillFrom: 'top'
			,getNextItemInMoreList: function(){
				var $items = this.moreList.find(this.navItemSelector);
				if($items.length){
					if(this.fillFrom === 'top'){
						return $items.first();
					}else{
						return $items.last();
					}
				}else{
					return null;
				}
			}
			,handleResize: function(){
				var doHandleResize = true;
				switch(typeof this.doHandleResize){
					case 'function':
						doHandleResize = this.doHandleResize.apply(this, arguments);
					break;
					default:
						doHandleResize = (this.doHandleResize);
					break;
				}
				if(doHandleResize){
					if(this.hasTooManyItems()){
						if(!this.moreItem.data('isAttached')){
							//--attach moreItem
							this.mainList.append(this.moreItem);
							this.moreItem.data('isAttached', true);
						}
						//--add items one by one to menu until not _isSectionNavTooNarrow
						do{
							this.pushItemToMoreList();
						}while(this.hasTooManyItems());
					}else{
						//--remove items one by one until not _isSectionNavTooNarrow
						do{
							this.popItemFromMoreList();
						}while(this.moreList.find(this.navItemSelector).length && !this.hasTooManyItems());

						//--reinsert one if too many are added
						if(this.hasTooManyItems()){
							this.pushItemToMoreList();
						}

						//--if item count reaches 0, detach moreItem
						if(this.moreItem.data('isAttached') && !this.moreList.find(this.navItemSelector).length){
							this.moreItem.detach();
							this.moreItem.data('isAttached', false);
						}
					}
					if(this.dropDown){
						this.dropDown.activate();
					}
				}
			}
			,handleResizeInInterval: function(){
				var _this = this;
				clearTimeout(_this.resizeTimeout);
				_this.resizeTimeout = setTimeout(function(){
					_this.handleResize();
				}, _this.resizeInterval);
			}
			,isActive: true
			,hasTooManyItems: function(){
				//--get actual method from private map based on 'hasTooManyItemsMethod' setting
				return __hasTooManyItemsMethods[this.hasTooManyItemsMethod].apply(this, arguments);
			}
			,hasTooManyItemsMethod: 'rightPos'
			,mainListSelector: '.navList.l-1'
			,minimumPadding: 20
			,moreItem: null
			,moreItemHTML: '
'
				+	'More'
				+	'
'
				+		'

'
				+	'
'
				+ '
'
			,moreList: null
			,moreListSelector: '.navList'
			,mainList: null
			,navItemSelector: '> .topItem'
			,popAllFromMoreList: function(){
				var $item;
				while(($item = this.getNextItemInMoreList())){
					this.popItemFromMoreList($item);
				}
				return this;
			}
			,popItemFromMoreList: function($item){
				if(!$item){
					$item = this.getNextItemInMoreList();
				}
				if($item){
					$item.detach();
					this.mainList.find(this.navItemSelector).last().before($item);
					if(this.dropDown && this.dropDown.doSizeAndCenter){
						this.dropDown.sizeAndCenter();
					}
				}
				return this;
			}
			,pushItemToMoreList: function(){
				var $item = (this.moreItem.data('isAttached'))
					? this.moreItem.prev()
					: this.mainList.find(this.navItemSelector).last();
				$item.detach();
				if(this.fillFrom === 'bottom'){
					this.moreList.append($item);
				}else{
					this.moreList.prepend($item);
				}
				if(this.dropDown && this.dropDown.doSizeAndCenter){
					this.dropDown.sizeAndCenter();
				}
				return this;
			}
			,resizeTimeout: null
			,resizeInterval: 200
			,topLevelSelector: '> .navItem'
		}
	});
	return __CollapsingNav;
});
<commit_msg>ui/collapsingnav: only activate/deactivate if needed<commit_after>/*
Class: CollapsingNav

Monitors the padding of nav elements inside a flexible horizontal nav container, putting items into a submenu when the padding gets too small (ie items get scrunched together).  Kinda limited in scope, in that it won't work for items with fixed padding, but that could be added as a different calculation method.

Example Usage:
	$sectionNavs = jQuery('.sectionNav');
	if($sectionNavs.length){
		$sectionNavs.each(function(){
			var $this = jQuery(this);
			var _collapsingNav = new __CollapsingNav({
				$: $this
				,doHandleResize: function(){
					return this.$.is('.current');
				}
			});
			var _dropDownMenu = new __.classes.DropDownMenu({
				elmWrapper: $this
				,itemSelector: '> .topItem'
			});
		});
	}

*/
/* global clearTimeout, define, setTimeout, window */
define(['tmclasses/tmclasses', 'jquery', './DropDown'], function(__tmclasses, jQuery, __DropDown){
	var __hasTooManyItemsMethods = {
		itemPadding: function(){
			var _this = this;
			var $navItems = this.mainList.find(this.navItemSelector);
			var _hasEnoughHeightDifference = false;
			var _hasTooLittlePadding = false;

			$navItems.each(function(){
				var $this = jQuery(this);
				var $topLevel = $this.find(_this.topLevelSelector);
				var _itemPadding = $this.outerWidth() - $topLevel.outerWidth();
				if(_itemPadding < _this.minimumPadding){
					_hasTooLittlePadding = true;
					return false;
				}
				//--check height to see if item wraps
				//-# must do this for each item, since individual items may have different font-size, etc
				var _html = $topLevel.html();
				$topLevel.html('x');
				var _singleLineHeight = $topLevel.outerHeight();
				$topLevel.html(_html);
				var _outerHeight = $topLevel.outerHeight();
				if(
					_singleLineHeight
					&& Math.abs(_outerHeight - _singleLineHeight) > 0.3 * _singleLineHeight
				){
					_hasEnoughHeightDifference = true;
					return false;
				}
			});
			return _hasEnoughHeightDifference || _hasTooLittlePadding;
		}
		,rightPos: function(){
			var _this = this;
			var $container = _this.getContainer();

			var containerRight = $container.offset().left + $container.outerWidth();
			var mainListRight = _this.mainList.offset().left + _this.mainList.outerWidth();

			return mainListRight > containerRight;
		}
	};

	var __CollapsingNav = __tmclasses.create({
		init: function(){
			this.__parent(arguments);
			var _this = this;

			if(!_this.moreItem){
				_this.moreItem = jQuery(_this.moreItemHTML);
			}
			_this.moreItem.data('isAttached', false);

			if(!_this.moreList){
				_this.moreList = _this.moreItem.find(_this.moreListSelector);
			}

			if(!_this.mainList){
				_this.mainList = _this.$.find(_this.mainListSelector);
			}

			if(_this.dropDown === undefined){
				_this.dropDown = new __DropDown(_this.getDropDownOpts());
			}

			if(_this.isActive){
				_this.activate();
			}
		}
		,properties: {
			$: null
			,$window: jQuery(window)
			,activate: function(){
				if(!this.isActive){
					//--attach resize listener
					this.$window.on('resize', jQuery.proxy(this.handleResizeInInterval, this));

					this.isActive = true;
				}
				this.handleResize();
			}
			,container: function(){
				this.container = jQuery('body');
				return this.container;
			}
			,dropDown: undefined
			,getContainer: function(){
				if(typeof this.container === 'function'){
					return this.container();
				}else{
					return this.container;
				}
			}
			,getDropDownOpts: function(_opts){
				return jQuery.extend({
					$: this.moreItem
					,itemSelector: 'this'
				}, _opts);
			}
			,deactivate: function(){
				if(this.isActive){
					clearTimeout(this.resizeTimeout);

					//--detach resize listener
					this.$window.off('resize', jQuery.proxy(this.handleResizeInInterval, this));

					if(this.dropDown){
						this.dropDown.deactivate();
					}

					this.isActive = false;

					this.popAllFromMoreList();
				}
			}
			,doHandleResize: true
			,fillFrom: 'top'
			,getNextItemInMoreList: function(){
				var $items = this.moreList.find(this.navItemSelector);
				if($items.length){
					if(this.fillFrom === 'top'){
						return $items.first();
					}else{
						return $items.last();
					}
				}else{
					return null;
				}
			}
			,handleResize: function(){
				var doHandleResize = true;
				switch(typeof this.doHandleResize){
					case 'function':
						doHandleResize = this.doHandleResize.apply(this, arguments);
					break;
					default:
						doHandleResize = (this.doHandleResize);
					break;
				}
				if(doHandleResize){
					if(this.hasTooManyItems()){
						if(!this.moreItem.data('isAttached')){
							//--attach moreItem
							this.mainList.append(this.moreItem);
							this.moreItem.data('isAttached', true);
						}
						//--add items one by one to menu until not _isSectionNavTooNarrow
						do{
							this.pushItemToMoreList();
						}while(this.hasTooManyItems());
					}else{
						//--remove items one by one until not _isSectionNavTooNarrow
						do{
							this.popItemFromMoreList();
						}while(this.moreList.find(this.navItemSelector).length && !this.hasTooManyItems());

						//--reinsert one if too many are added
						if(this.hasTooManyItems()){
							this.pushItemToMoreList();
						}

						//--if item count reaches 0, detach moreItem
						if(this.moreItem.data('isAttached') && !this.moreList.find(this.navItemSelector).length){
							this.moreItem.detach();
							this.moreItem.data('isAttached', false);
						}
					}
					if(this.dropDown){
						this.dropDown.activate();
					}
				}
			}
			,handleResizeInInterval: function(){
				var _this = this;
				clearTimeout(_this.resizeTimeout);
				_this.resizeTimeout = setTimeout(function(){
					_this.handleResize();
				}, _this.resizeInterval);
			}
			,isActive: true
			,hasTooManyItems: function(){
				//--get actual method from private map based on 'hasTooManyItemsMethod' setting
				return __hasTooManyItemsMethods[this.hasTooManyItemsMethod].apply(this, arguments);
			}
			,hasTooManyItemsMethod: 'rightPos'
			,mainListSelector: '.navList.l-1'
			,minimumPadding: 20
			,moreItem: null
			,moreItemHTML: '
'
				+	'More'
				+	'
'
				+		'

'
				+	'
'
				+ '
'
			,moreList: null
			,moreListSelector: '.navList'
			,mainList: null
			,navItemSelector: '> .topItem'
			,popAllFromMoreList: function(){
				var $item;
				while(($item = this.getNextItemInMoreList())){
					this.popItemFromMoreList($item);
				}
				return this;
			}
			,popItemFromMoreList: function($item){
				if(!$item){
					$item = this.getNextItemInMoreList();
				}
				if($item){
					$item.detach();
					this.mainList.find(this.navItemSelector).last().before($item);
					if(this.dropDown && this.dropDown.doSizeAndCenter){
						this.dropDown.sizeAndCenter();
					}
				}
				return this;
			}
			,pushItemToMoreList: function(){
				var $item = (this.moreItem.data('isAttached'))
					? this.moreItem.prev()
					: this.mainList.find(this.navItemSelector).last();
				$item.detach();
				if(this.fillFrom === 'bottom'){
					this.moreList.append($item);
				}else{
					this.moreList.prepend($item);
				}
				if(this.dropDown && this.dropDown.doSizeAndCenter){
					this.dropDown.sizeAndCenter();
				}
				return this;
			}
			,resizeTimeout: null
			,resizeInterval: 200
			,topLevelSelector: '> .navItem'
		}
	});
	return __CollapsingNav;
});
<|endoftext|>


<commit_before>/*!
 * Subtitle.js
 * Parse and manipulate SRT (SubRip)
 * https://github.com/gsantiago/subtitle.js
 *
 * @version 0.0.7
 * @author Guilherme Santiago
*/

/**
 * Dependencies
*/
var _ = require('underscore');

/**
 * @constructor
 * @param {String} Optional SRT content to be parsed
*/
function Subtitle (srt) {
  this._subtitles = [];

  if (srt) {
    this.parse(srt);
  }
}

/**
 * SRT parser
 *
 * @public
 * @param {String} SRT
*/
Subtitle.prototype.parse = function (srt) {
  var subs = [];
  var index;
  var time;
  var text;
  var start;
  var end;

  if (!srt) {
    throw new Error('No SRT to parse');
  }

  srt = srt.split('\n');

  srt.forEach(function (line) {
    line = line.toString();

    // if we don't have an index, so we should expect an index
    if (!index) {
      if (/^\d+$/.test(line)) {
        index = parseInt(line);
        return;
      }
    }

    // now we have to check for the time
    if (!time) {
      var match = line.match(/^(\d{2}:\d{2}:\d{2},\d{3}) --> (\d{2}:\d{2}:\d{2},\d{3})$/);
      if (match) {
        start = match[1];
        end = match[2];
        time = true;
        return;
      }
    }

    // now we get all the strings until we get an empty line
    if (line.trim() === '') {
      subs.push({
        index: index,
        start: start,
        end: end,
        duration: Subtitle.toMS(end) - Subtitle.toMS(start),
        text: text
      });
      index = time = start = end = text = null;
    } else {
      if (!text) {
        text = line;
      } else {
        text += '\n' + line;
      }
    }

  });

  this._subtitles = subs;

  return this;
};

/**
 * Add a caption
 * You have to pass an object containing the following data:
 * start - The start time
 * end - The end time
 * text - The caption text
 *
 * The start and end time support two patterns:
 * The SRT: '00:00:24,400'
 * Or a positive integer representing milliseconds
 *
 * @public
 * @param {Object} Caption data
*/
Subtitle.prototype.add = function (caption) {
  if (!caption.start || !caption.end || !caption.text) {
    throw new Error('Invalid caption data');
  }

  for (var prop in caption) {
    if (!caption.hasOwnProperty(prop) || prop === 'text') {
      continue;
    }

    if (prop === 'start' || prop === 'end') {
      if (/^(\d{2}):(\d{2}):(\d{2}),(\d{3})$/.test(caption[prop])) {
        continue;
      }
      if (/^\d+$/.test(caption[prop])) {
        caption[prop] = Subtitle.toSrtTime(caption[prop]);
      } else {
        throw new Error('Invalid caption time format');
      }
    }
  }

  this._subtitles.push({
    index: this._subtitles.length + 1,
    start: caption.start,
    end: caption.end,
    duration: Subtitle.toMS(caption.end) - Subtitle.toMS(caption.start),
    text: caption.text
  });

  return this;
};

/**
 * Convert the SRT time format to milliseconds
 *
 * @static
 * @param {String} SRT time format
*/
Subtitle.toMS = function (time) {
  var match = time.match(/^(\d{2}):(\d{2}):(\d{2}),(\d{3})$/);

  if (!match) {
    throw new Error('Invalid SRT time format');
  }

  var hours = parseInt(match[1], 10);
  var minutes = parseInt(match[2], 10);
  var seconds = parseInt(match[3], 10);
  var milliseconds = parseInt(match[4], 10);

  hours *= 3600000;
  minutes *= 60000;
  seconds *= 1000;

  return hours + minutes + seconds + milliseconds;
};

/**
 * Convert milliseconds to SRT time format
 *
 * @static
 * @param {Integer} Milleseconds
*/
Subtitle.toSrtTime = function (time) {
  if (!/^\d+$/.test(time.toString())) {
    throw new Error('Time should be an Integer value in milliseconds');
  }

  time = parseInt(time);

  var date = new Date(0, 0, 0, 0, 0, 0, time);

  var hours = date.getHours() < 10
    ? '0' + date.getHours()
    : date.getHours();

  var minutes = date.getMinutes() < 10
    ? '0' + date.getMinutes()
    : date.getMinutes();

  var seconds = date.getSeconds() < 10
    ? '0' + date.getSeconds()
    : date.getSeconds();

  var ms = time - ((hours * 3600000) + (minutes * 60000) + (seconds * 1000));

  if (ms < 100 && ms > 10) {
    ms = '0' + ms;
  } else if (ms < 10) {
    ms = '00' + ms;
  }

  var srtTime = hours + ':' + minutes + ':' + seconds + ',' + ms;

  return srtTime;
};

/**
 * Return the subtitles
 *
 * @param {Object} Options
 * @returns {Array} Subtitles
*/
Subtitle.prototype.getSubtitles = function (options) {
  var subtitles = this._subtitles;

  var defaults = {
    timeFormat: 'srt',
    duration: false
  };

  options = _.extendOwn(defaults, options);

  if (options.timeFormat === 'ms') {
    subtitles = _.map(subtitles, function (caption) {
      caption.start = Subtitle.toMS(caption.start);
      caption.end = Subtitle.toMS(caption.end);
      return caption;
    });
  }

  if (!options.duration) {
    subtitles = _.map(subtitles, function (caption) {
      delete caption.duration;
      return caption;
    });
  }

  return subtitles;
};

/**
 * Returns the subtitles in SRT string
 * @returns {String} srt
*/
Subtitle.prototype.stringfy = function () {
  var self = this;
  var buffer = '';

  this._subtitles.forEach(function (caption, index) {
    if (index > 0) {
      buffer += '\n';
    }
    buffer += caption.index;
    buffer += '\n';
    buffer += caption.start + ' --> ' + caption.end;
    buffer += '\n';
    buffer += caption.text;
    buffer += '\n';
  });

  return buffer;
};

module.exports = Subtitle;
<commit_msg>Add Resync method<commit_after>/*!
 * Subtitle.js
 * Parse and manipulate SRT (SubRip)
 * https://github.com/gsantiago/subtitle.js
 *
 * @version 0.0.7
 * @author Guilherme Santiago
*/

/**
 * Dependencies
*/
var _ = require('underscore');

/**
 * @constructor
 * @param {String} Optional SRT content to be parsed
*/
function Subtitle (srt) {
  this._subtitles = [];

  if (srt) {
    this.parse(srt);
  }
}

/**
 * SRT parser
 *
 * @public
 * @param {String} SRT
*/
Subtitle.prototype.parse = function (srt) {
  var subs = [];
  var index;
  var time;
  var text;
  var start;
  var end;

  if (!srt) {
    throw new Error('No SRT to parse');
  }

  srt = srt.split('\n');

  srt.forEach(function (line) {
    line = line.toString();

    // if we don't have an index, so we should expect an index
    if (!index) {
      if (/^\d+$/.test(line)) {
        index = parseInt(line);
        return;
      }
    }

    // now we have to check for the time
    if (!time) {
      var match = line.match(/^(\d{2}:\d{2}:\d{2},\d{3}) --> (\d{2}:\d{2}:\d{2},\d{3})$/);
      if (match) {
        start = match[1];
        end = match[2];
        time = true;
        return;
      }
    }

    // now we get all the strings until we get an empty line
    if (line.trim() === '') {
      subs.push({
        index: index,
        start: start,
        end: end,
        duration: Subtitle.toMS(end) - Subtitle.toMS(start),
        text: text
      });
      index = time = start = end = text = null;
    } else {
      if (!text) {
        text = line;
      } else {
        text += '\n' + line;
      }
    }

  });

  this._subtitles = subs;

  return this;
};

/**
 * Add a caption
 * You have to pass an object containing the following data:
 * start - The start time
 * end - The end time
 * text - The caption text
 *
 * The start and end time support two patterns:
 * The SRT: '00:00:24,400'
 * Or a positive integer representing milliseconds
 *
 * @public
 * @param {Object} Caption data
*/
Subtitle.prototype.add = function (caption) {
  if (!caption.start || !caption.end || !caption.text) {
    throw new Error('Invalid caption data');
  }

  for (var prop in caption) {
    if (!caption.hasOwnProperty(prop) || prop === 'text') {
      continue;
    }

    if (prop === 'start' || prop === 'end') {
      if (/^(\d{2}):(\d{2}):(\d{2}),(\d{3})$/.test(caption[prop])) {
        continue;
      }
      if (/^\d+$/.test(caption[prop])) {
        caption[prop] = Subtitle.toSrtTime(caption[prop]);
      } else {
        throw new Error('Invalid caption time format');
      }
    }
  }

  this._subtitles.push({
    index: this._subtitles.length + 1,
    start: caption.start,
    end: caption.end,
    duration: Subtitle.toMS(caption.end) - Subtitle.toMS(caption.start),
    text: caption.text
  });

  return this;
};

/**
 * Convert the SRT time format to milliseconds
 *
 * @static
 * @param {String} SRT time format
*/
Subtitle.toMS = function (time) {
  var match = time.match(/^(\d{2}):(\d{2}):(\d{2}),(\d{3})$/);

  if (!match) {
    throw new Error('Invalid SRT time format');
  }

  var hours = parseInt(match[1], 10);
  var minutes = parseInt(match[2], 10);
  var seconds = parseInt(match[3], 10);
  var milliseconds = parseInt(match[4], 10);

  hours *= 3600000;
  minutes *= 60000;
  seconds *= 1000;

  return hours + minutes + seconds + milliseconds;
};

/**
 * Convert milliseconds to SRT time format
 *
 * @static
 * @param {Integer} Milleseconds
*/
Subtitle.toSrtTime = function (time) {
  if (!/^\d+$/.test(time.toString())) {
    throw new Error('Time should be an Integer value in milliseconds');
  }

  time = parseInt(time);

  var date = new Date(0, 0, 0, 0, 0, 0, time);

  var hours = date.getHours() < 10
    ? '0' + date.getHours()
    : date.getHours();

  var minutes = date.getMinutes() < 10
    ? '0' + date.getMinutes()
    : date.getMinutes();

  var seconds = date.getSeconds() < 10
    ? '0' + date.getSeconds()
    : date.getSeconds();

  var ms = time - ((hours * 3600000) + (minutes * 60000) + (seconds * 1000));

  if (ms < 100 && ms > 10) {
    ms = '0' + ms;
  } else if (ms < 10) {
    ms = '00' + ms;
  }

  var srtTime = hours + ':' + minutes + ':' + seconds + ',' + ms;

  return srtTime;
};

/**
 * Return the subtitles
 *
 * @param {Object} Options
 * @returns {Array} Subtitles
*/
Subtitle.prototype.getSubtitles = function (options) {
  var subtitles = this._subtitles;

  var defaults = {
    timeFormat: 'srt',
    duration: false
  };

  options = _.extendOwn(defaults, options);

  if (options.timeFormat === 'ms') {
    subtitles = _.map(subtitles, function (caption) {
      caption.start = Subtitle.toMS(caption.start);
      caption.end = Subtitle.toMS(caption.end);
      return caption;
    });
  }

  if (!options.duration) {
    subtitles = _.map(subtitles, function (caption) {
      delete caption.duration;
      return caption;
    });
  }

  return subtitles;
};

/**
 * Returns the subtitles in SRT string
 * @returns {String} srt
*/
Subtitle.prototype.stringfy = function () {
  var self = this;
  var buffer = '';

  this._subtitles.forEach(function (caption, index) {
    if (index > 0) {
      buffer += '\n';
    }
    buffer += caption.index;
    buffer += '\n';
    buffer += caption.start + ' --> ' + caption.end;
    buffer += '\n';
    buffer += caption.text;
    buffer += '\n';
  });

  return buffer;
};

/**
 * Resync the captions
 * @param {Integer} Time in milleseconds
*/
Subtitle.prototype.resync = function (time) {
  if (!/(-|\+)?\d+/.test(time.toString())) {
    throw new Error('Invalid time: ' + time + '.Expected a valid integer');
  }

  time = parseInt(time, 10);

  this._subtitles = _.map(this._subtitles, function (caption) {
    var start = Subtitle.toMS(caption.start);
    var end = Subtitle.toMS(caption.end);

    start = start + time;
    end = end + time;

    caption.start = start < 0
      ? Subtitle.toSrtTime(0)
      : Subtitle.toSrtTime(start);

    caption.end = end < 0
      ? Subtitle.toSrtTime(0)
      : Subtitle.toSrtTime(end);

    return caption;
  });

  return this;
};

module.exports = Subtitle;
<|endoftext|>


<commit_before>(function(Instajam) {

  'use strict';

  // # Initialization

  Instajam.init = function(options) {

    options = options || {};

    // Throw an error if either the client ID or the
    // redirect URI isn't provided.

    if (!options.clientId || !options.redirectUri) {
      throw new InstajamError("Client ID and Redirect URI are required.");
    }

    // If the app is requesting additional
    // scopes, build a string to append to
    // the auth URL.
    if (options.scope && typeof options.scope === 'object') {
      this.scope = '&scope=' + options.scope.join('+');
    } else {
      this.scope = '&scope=basic';
    }

    // Build an authentication URL using
    // constructor parameters.
    this.authUrl = 'https://instagram.com/oauth/authorize/?client_id=' + options.clientId + '&redirect_uri=' + options.redirectUri + '&response_type=token' + (this.scope || '');

    // When the library is initialized, verify whether
    // a user is currently authenticated.
    this.authenticate();

    return this;

  };

  // # Authentication

  // Attempts to authenticate a user via
  // localStorage data or by parsing data
  // from the URL hash.

  Instajam.authenticate = function() {

    // First, check if a localStorage key
    // exists for the access_token...

    if (localStorage.getItem('instagram_access_token')) {

      // ...and if there is, set the
      // authenticated property to true.
      this.authenticated = true;

    // If there is no localStorage key...

    } else {

      // ...then check if there's a match
      // for access_token in the URL hash.
      if (hashParam('access_token')) {

        // If we can parse the access_token from
        // the URL hash, set the localStorage param...
        localStorage.setItem('instagram_access_token', hashParam('access_token', true));

        // ...and set the authenticated property to true
        this.authenticated = true;

      } else {

        // Otherwise, if there is no localStorage
        // key and there is nothing to parse from
        // the hash, set the authenticated
        // property to false
        this.authenticated = false;

      }

    }

  };

  // Effectively de-authenticates the current
  // user by removing their access token from
  // localStorage and setting the authenticated
  // property to false. This does **not**
  // revoke your app's permissions on the server.

  Instajam.deauthenticate = function() {
    localStorage.removeItem('instagram_access_token');
    this.authenticated = false;
  };

  // # Endpoints

  // ## Users

  var User = function() {};
  var Self = function() {};
  User.prototype.self = new Self();

  // ### Fetching the authenticated users's profile

  Self.prototype.profile = function(callback) {
    request({
      url: 'users/self',
      success: callback
    });
  };

  // ### Fetching the authenticated user's media

  Self.prototype.media = function(options, callback) {

    // Make the options argument optional
    if (typeof options === 'function') {
      callback = options;
      options = null;
    }

    // Make a request to the API
    request({
      url: 'users/self/media/recent',
      data: options,
      success: callback
    });

  };

  // ### Fetching the authenticated user's activity feed

  Self.prototype.feed = function(options, callback) {

    // Make the options argument optional
    if (typeof options === 'function') {
      callback = options;
      options = null;
    }

    // Make a request to the API
    request({
      url: 'users/self/feed',
      data: options,
      success: callback
    });

  };

  // ### Fetching the authenticated user's favorites.

  Self.prototype.favorites = function(options, callback) {

    // Make the options argument optional
    if (typeof options === 'function') {
      callback = options;
      options = null;
    }

    // Make a request to the API
    request({
      url: 'users/self/media/liked',
      data: options,
      success: callback
    });

  };

  // ### Fetching a list of relationship requests for the currently authenticated user

  User.prototype.requests = function(callback) {

    // Make a request to the API
    request({
      url: 'users/self/requested-by',
      success: callback
    });

  };

  // ### Getting a relationship information for the currently authenciated user and a given user ID

  User.prototype.relationshipWith = function(id, callback) {

    // Make a request to the API
    request({
      url: 'users/' + id + '/relationship',
      success: callback
    });

  };

  // ### Fetching the profile of a user by ID or username

  User.prototype.get = function(id, callback) {

    // Require that an ID or username be passed
    if (!id) {
      throw new InstajamError('A user\'s ID or username is required for user.get()');
    }

    if (typeof id === 'number') {

      // Make a request to the API
      request({
        url: 'users/' + id,
        success: callback
      });

    } else if (typeof id === 'string') {

      // Make a request to the API
      User.prototype.search.call(this, id, {}, function(result) {

        // If the initial user search yields any
        // results, then just return the first, but
        // otherwise return nothing.
        if (result.data && result.data.length === 1) {
          result = result.data[0];
        } else {
          result = false;
        }

        // Call the initial callback, passing the result
        if (typeof callback === 'function') {
          callback(result);
        }

      });

    }

  };

  // ### Fetching the media of a user ID or username

  User.prototype.media = function(id, options, callback) {

    // Require that an ID be passed

    if (!id) {
      throw new InstajamError('A user\'s ID or username is required for user.media()');
    }

    // Make the options argument optional

    if (typeof options === 'function' && !callback) {
      callback = options;
      options = null;
    }

    // If we're looking up the user by ID...

    if (typeof id === 'number') {

      // Make a request to that API
      request({
        url: 'users/' + id + '/media/recent',
        data: options,
        success: callback
      });

    }

    // Or rather looking up the user by username...

    else if (typeof id === 'string') {

      // ...then first search for the username...
      User.prototype.search.call(this, id, {}, function(result) {

        // If the initial user search yields any
        // results, then just return the first, but
        // otherwise return nothing.
        if (result.data && result.data.length > 0) {
          result = result.data[0];
        } else {
          result = false;
        }

        if (result) {

          // Make a request to that API
          request({
            url: 'users/' + result.id + '/media/recent',
            data: options,
            success: callback
          });

        } else {

          if (typeof callback === 'function') {
            callback(result);
          }

        }

      });

    }

  };

  // ### Searching for users by username

  User.prototype.search = function(term, options, callback) {

    // Require that a search term be passed
    if (!term) {
      throw new InstajamError('A search term is required for user.search()');
    }

    // Make the options argument optional
    if (typeof options === 'function') {
      callback = options;
      options = {};
    }

    // Add the search term to the options object
    options.q = term;

    // Make a request to the API
    request({
      url: 'users/search',
      data: options,
      success: callback
    });

  };

  // ### Fetching a list of users that user [id] follows

  User.prototype.follows = function(id, callback) {

    // Make a request to the API
    request({
      url: 'users/' + id + '/follows',
      success: callback
    });

  };

  // ### Fetching a list of followers of user [id]

  User.prototype.following = function(id, callback) {

    // Make a request to the API
    request({
      url: 'users/' + id + '/followed-by',
      success: callback
    });

  };

  // ## Media

  var Media = function() {};

  Media.prototype.get = function(id, callback) {

    // Make a request to the API
    request({
      url: 'media/' + id,
      success: callback
    });

  };

  Media.prototype.search = function(options, callback) {

    options = options || {};

    // Require that a latitude and longitude are
    // passed in, at a minimum.
    if (!options.lat || !options.lng) {
      throw new InstajamError('A latitude AND a longitude are required for media.search()');
    }

    // Make a request to the API
    request({
      url: 'media/search',
      data: options,
      success: callback
    });

  };

  Media.prototype.popular = function(callback) {

    // Make a request to the API
    request({
      url: 'media/popular',
      success: callback
    });

  };

  Media.prototype.comments = function(id, callback) {

    if (!id) {
      throw new InstajamError('A media ID is required for media.comments()');
    }

    // Make a request to the API
    request({
      url: 'media/' + id + '/comments',
      success: callback
    });

  };

  Media.prototype.likes = function(id, callback) {

    if (!id) {
      throw new InstajamError('A media ID is required for media.likes()');
    }

    // Make a request to the API
    request({
      url: 'media/' + id + '/likes',
      success: callback
    });

  };

  // ## Tags

  var Tag = function() {};

  Tag.prototype.get = function(name, callback) {

    // We need at least a tag name to get information for
    if (!name) {
      throw new InstajamError('A tag name is required for tag.get()');
    }

    // Make a request to the API
    request({
      url: 'tags/' + name,
      success: callback
    });

  };

  Tag.prototype.media = function(name, options, callback) {

    // We need at least a tag name to work with
    if (!name) {
      throw new InstajamError('A tag name is required for tag.media()');
    }

    // Make the options argument optional
    if (typeof options === 'function' && !callback) {
      callback = options;
      options = {};
    }

    // Make a request to the API
    request({
      url: 'tags/' + name + '/media/recent',
      data: options,
      success: callback
    });

  };

  Tag.prototype.search = function(term, callback) {

    // We need at least a tag string to search for
    if (!term) {
      throw new InstajamError('A tag name is required for tag.search()');
    }

    var options = {
      q: term
    };

    // Make a request to the API
    request({
      url: '/tags/search',
      data: options,
      success: callback
    });

  };

  // ## Locations

  var Location = function() {};

  Location.prototype.get = function(id, callback) {

    // We need at least a location ID to work with
    if (!id) {
      throw new InstajamError('An ID is required for location.get()');
    }

    // Make a request to the API
    request({
      url: 'locations/' + id,
      success: callback
    });

  };

  Location.prototype.media = function(id, options, callback) {

    // We need at least a location ID to work with
    if (!id) {
      throw new InstajamError('An ID is required for location.get()');
    }

    // Make the options argument optional
    if (typeof options === 'function' && !callback) {
      callback = options;
      options = {};
    }

    // Make a request to the API
    request({
      url: 'locations/' + id + '/media/recent',
      success: callback
    });

  };

  Location.prototype.search = function(options, callback) {

    options = options || {};

    // We need at LEAST a lat/lng pair, or a Foursquare ID to work with
    if ( (!options.lat || !options.lng) && !options.foursquare_v2_id) {
      throw new InstajamError('A latitude and longitude OR a Foursquare place ID is required for location.search()');
    }

    // Make a request to the API
    request({
      url: 'locations/search',
      data: options,
      success: callback
    });

  };

  // ## Geographies

  var Geography = function() {};

  Geography.prototype.media = function(id, options, callback) {

    // We need at least a Geography ID to work with
    if (!id) {
      throw new InstajamError('A Geography ID is required for geography.get()');
    }

    // The options argument defaults to an empty object
    options = options || {};

    // Make a request to the API
    request({
      url: 'geographies/' + id + '/media/recent',
      data: options,
      success: callback
    });

  };

  // # Helpers

  // Returns the client-specific authentication URL that is created upon initialization.

  // Parses a given parameter from the browsers hash.
  // Optionally, the parameter can be removed from
  // the URL upon successful matching.

  function hashParam (param, remove) {

    // Create a RegExp object for parsing params
    var regex = new RegExp("(?:&|#)" + param + "=([a-z0-9._-]+)", "i");

    // Look for matches in the windows hash
    var matches = window.location.hash.match(regex);

    // If matches are found...
    if (matches) {

      // ...then remove the parameter if specified
      if (remove) {
        var removeRegex = new RegExp("(?:&|#)" + param + "=" + matches[1], "i");
        window.location.hash = window.location.hash.replace(removeRegex, '');
      }

      // ...and return the first matching param
      return matches[1];

    }

    // Otherwise return false if no matching params are found
    return false;

  }

  // Makes JSONP requests to the Instagram API

  function request (options) {

    var urlBase = 'https://api.instagram.com/v1/',
        callbackName = 'instajam' + Math.round(new Date().getTime() / 1000) + Math.floor(Math.random() * 100);

    options = options || {};
    options.data = options.data || {};
    options.data.access_token = localStorage.getItem('instagram_access_token');
    options.data.callback = callbackName;

    var queryString = serializeParams(options.data);

    if (options.url) {
      options.url = urlBase + options.url + '?' + queryString;

      window[callbackName] = function(data) {

        if (typeof options.success === 'function') {
          options.success(data);
        }

        script.parentNode.removeChild(script);
        delete window[callbackName];
      };

      var script = document.createElement('script');
      script.type = 'text/javascript';
      script.src = options.url;
      document.getElementsByTagName('body')[0].appendChild(script);

    } else {
      throw new InstajamError("Instajam:: Missing request URL");
    }

  }

  // Given a JavaScript object, return a
  // string suitable for passing in a URL

  function serializeParams (obj) {
    var str = [];
    for (var p in obj) {
      str.push(encodeURIComponent(p) + '=' + encodeURIComponent(obj[p]));
    }
    return str.join('&');
  }

  // Define a custom error object

  function InstajamError (message) {
    this.name = "InstajamError";
    this.message = message || '';
  }

  InstajamError.prototype = Error.prototype;

  // Return new instances of the endpoint
  // helpers as top-level keys

  Instajam.user = new User();
  Instajam.media = new Media();
  Instajam.tag = new Tag();
  Instajam.location = new Location();
  // Instajam.geography = new Geography();

}(window.Instajam = window.Instajam || {}));<commit_msg>Update instajam.js<commit_after>(function(Instajam) {

  'use strict';

  // # Initialization

  Instajam.init = function(options) {

    options = options || {};

    // Throw an error if either the client ID or the
    // redirect URI isn't provided.

    if (!options.clientId || !options.redirectUri) {
      throw new InstajamError("Client ID and Redirect URI are required.");
    }

    // If the app is requesting additional
    // scopes, build a string to append to
    // the auth URL.
    if (options.scope && typeof options.scope === 'object') {
      this.scope = '&scope=' + options.scope.join('+');
    } else {
      this.scope = '&scope=basic';
    }

    // Build an authentication URL using
    // constructor parameters.
    this.authUrl = 'https://instagram.com/oauth/authorize/?client_id=' + options.clientId + '&redirect_uri=' + options.redirectUri + '&response_type=token' + (this.scope || '');

    // When the library is initialized, verify whether
    // a user is currently authenticated.
    this.authenticate();

    return this;

  };

  // # Authentication

  // Attempts to authenticate a user via
  // localStorage data or by parsing data
  // from the URL hash.

  Instajam.authenticate = function() {

    // First, check if a localStorage key
    // exists for the access_token...

    if (localStorage.getItem('instagram_access_token')) {

      // ...and if there is, set the
      // authenticated property to true.
      this.authenticated = true;

    // If there is no localStorage key...

    } else {

      // ...then check if there's a match
      // for access_token in the URL hash.
      if (hashParam('access_token')) {

        // If we can parse the access_token from
        // the URL hash, set the localStorage param...
        localStorage.setItem('instagram_access_token', hashParam('access_token', true));

        // ...and set the authenticated property to true
        this.authenticated = true;

      } else {

        // Otherwise, if there is no localStorage
        // key and there is nothing to parse from
        // the hash, set the authenticated
        // property to false
        this.authenticated = false;

      }

    }

  };

  // Effectively de-authenticates the current
  // user by removing their access token from
  // localStorage and setting the authenticated
  // property to false. This does **not**
  // revoke your app's permissions on the server.

  Instajam.deauthenticate = function() {
    localStorage.removeItem('instagram_access_token');
    this.authenticated = false;
  };

  // # Endpoints

  // ## Users

  var User = function() {};
  var Self = function() {};
  User.prototype.self = new Self();

  // ### Fetching the authenticated users's profile

  Self.prototype.profile = function(callback) {
    request({
      url: 'users/self',
      success: callback
    });
  };

  // ### Fetching the authenticated user's media

  Self.prototype.media = function(options, callback) {

    // Make the options argument optional
    if (typeof options === 'function') {
      callback = options;
      options = null;
    }

    // Make a request to the API
    request({
      url: 'users/self/media/recent',
      data: options,
      success: callback
    });

  };

  // ### Fetching the authenticated user's activity feed

  Self.prototype.feed = function(options, callback) {

    // Make the options argument optional
    if (typeof options === 'function') {
      callback = options;
      options = null;
    }

    // Make a request to the API
    request({
      url: 'users/self/feed',
      data: options,
      success: callback
    });

  };

  // ### Fetching the authenticated user's favorites.

  Self.prototype.favorites = function(options, callback) {

    // Make the options argument optional
    if (typeof options === 'function') {
      callback = options;
      options = null;
    }

    // Make a request to the API
    request({
      url: 'users/self/media/liked',
      data: options,
      success: callback
    });

  };

  // ### Fetching a list of relationship requests for the currently authenticated user

  User.prototype.requests = function(callback) {

    // Make a request to the API
    request({
      url: 'users/self/requested-by',
      success: callback
    });

  };

  // ### Getting a relationship information for the currently authenciated user and a given user ID

  User.prototype.relationshipWith = function(id, callback) {

    // Make a request to the API
    request({
      url: 'users/' + id + '/relationship',
      success: callback
    });

  };

  // ### Fetching the profile of a user by ID or username

  User.prototype.get = function(id, callback) {

    // Require that an ID or username be passed
    if (!id) {
      throw new InstajamError('A user\'s ID or username is required for user.get()');
    }

    if (typeof id === 'number') {

      // Make a request to the API
      request({
        url: 'users/' + id,
        success: callback
      });

    } else if (typeof id === 'string') {

      // Make a request to the API
      User.prototype.search.call(this, id, {}, function(result) {

        // Go through the results and check for
        // a perfect match, then query again with that user's ID
        // otherwise return nothing.
        if (result.data) {
            var userIds = result.data;
            result = false;

            for (var i = 0; i < userIds.length; i++) {
                if (userIds[i].username == id) {
                    User.prototype.get(parseInt(userIds[i].id), callback);
                    return;
                }
            }
        } else {
          result = false;
        }

        // Call the initial callback, passing the result
        if (typeof callback === 'function') {
          callback(result);
        }

      });

    }

  };

  // ### Fetching the media of a user ID or username

  User.prototype.media = function(id, options, callback) {

    // Require that an ID be passed

    if (!id) {
      throw new InstajamError('A user\'s ID or username is required for user.media()');
    }

    // Make the options argument optional

    if (typeof options === 'function' && !callback) {
      callback = options;
      options = null;
    }

    // If we're looking up the user by ID...

    if (typeof id === 'number') {

      // Make a request to that API
      request({
        url: 'users/' + id + '/media/recent',
        data: options,
        success: callback
      });

    }

    // Or rather looking up the user by username...

    else if (typeof id === 'string') {

      // ...then first search for the username...
      User.prototype.search.call(this, id, {}, function(result) {

        // If the initial user search yields any
        // results, then just return the first, but
        // otherwise return nothing.
        if (result.data && result.data.length > 0) {
          result = result.data[0];
        } else {
          result = false;
        }

        if (result) {

          // Make a request to that API
          request({
            url: 'users/' + result.id + '/media/recent',
            data: options,
            success: callback
          });

        } else {

          if (typeof callback === 'function') {
            callback(result);
          }

        }

      });

    }

  };

  // ### Searching for users by username

  User.prototype.search = function(term, options, callback) {

    // Require that a search term be passed
    if (!term) {
      throw new InstajamError('A search term is required for user.search()');
    }

    // Make the options argument optional
    if (typeof options === 'function') {
      callback = options;
      options = {};
    }

    // Add the search term to the options object
    options.q = term;

    // Make a request to the API
    request({
      url: 'users/search',
      data: options,
      success: callback
    });

  };

  // ### Fetching a list of users that user [id] follows

  User.prototype.follows = function(id, callback) {

    // Make a request to the API
    request({
      url: 'users/' + id + '/follows',
      success: callback
    });

  };

  // ### Fetching a list of followers of user [id]

  User.prototype.following = function(id, callback) {

    // Make a request to the API
    request({
      url: 'users/' + id + '/followed-by',
      success: callback
    });

  };

  // ## Media

  var Media = function() {};

  Media.prototype.get = function(id, callback) {

    // Make a request to the API
    request({
      url: 'media/' + id,
      success: callback
    });

  };

  Media.prototype.search = function(options, callback) {

    options = options || {};

    // Require that a latitude and longitude are
    // passed in, at a minimum.
    if (!options.lat || !options.lng) {
      throw new InstajamError('A latitude AND a longitude are required for media.search()');
    }

    // Make a request to the API
    request({
      url: 'media/search',
      data: options,
      success: callback
    });

  };

  Media.prototype.popular = function(callback) {

    // Make a request to the API
    request({
      url: 'media/popular',
      success: callback
    });

  };

  Media.prototype.comments = function(id, callback) {

    if (!id) {
      throw new InstajamError('A media ID is required for media.comments()');
    }

    // Make a request to the API
    request({
      url: 'media/' + id + '/comments',
      success: callback
    });

  };

  Media.prototype.likes = function(id, callback) {

    if (!id) {
      throw new InstajamError('A media ID is required for media.likes()');
    }

    // Make a request to the API
    request({
      url: 'media/' + id + '/likes',
      success: callback
    });

  };

  // ## Tags

  var Tag = function() {};

  Tag.prototype.get = function(name, callback) {

    // We need at least a tag name to get information for
    if (!name) {
      throw new InstajamError('A tag name is required for tag.get()');
    }

    // Make a request to the API
    request({
      url: 'tags/' + name,
      success: callback
    });

  };

  Tag.prototype.media = function(name, options, callback) {

    // We need at least a tag name to work with
    if (!name) {
      throw new InstajamError('A tag name is required for tag.media()');
    }

    // Make the options argument optional
    if (typeof options === 'function' && !callback) {
      callback = options;
      options = {};
    }

    // Make a request to the API
    request({
      url: 'tags/' + name + '/media/recent',
      data: options,
      success: callback
    });

  };

  Tag.prototype.search = function(term, callback) {

    // We need at least a tag string to search for
    if (!term) {
      throw new InstajamError('A tag name is required for tag.search()');
    }

    var options = {
      q: term
    };

    // Make a request to the API
    request({
      url: '/tags/search',
      data: options,
      success: callback
    });

  };

  // ## Locations

  var Location = function() {};

  Location.prototype.get = function(id, callback) {

    // We need at least a location ID to work with
    if (!id) {
      throw new InstajamError('An ID is required for location.get()');
    }

    // Make a request to the API
    request({
      url: 'locations/' + id,
      success: callback
    });

  };

  Location.prototype.media = function(id, options, callback) {

    // We need at least a location ID to work with
    if (!id) {
      throw new InstajamError('An ID is required for location.get()');
    }

    // Make the options argument optional
    if (typeof options === 'function' && !callback) {
      callback = options;
      options = {};
    }

    // Make a request to the API
    request({
      url: 'locations/' + id + '/media/recent',
      success: callback
    });

  };

  Location.prototype.search = function(options, callback) {

    options = options || {};

    // We need at LEAST a lat/lng pair, or a Foursquare ID to work with
    if ( (!options.lat || !options.lng) && !options.foursquare_v2_id) {
      throw new InstajamError('A latitude and longitude OR a Foursquare place ID is required for location.search()');
    }

    // Make a request to the API
    request({
      url: 'locations/search',
      data: options,
      success: callback
    });

  };

  // ## Geographies

  var Geography = function() {};

  Geography.prototype.media = function(id, options, callback) {

    // We need at least a Geography ID to work with
    if (!id) {
      throw new InstajamError('A Geography ID is required for geography.get()');
    }

    // The options argument defaults to an empty object
    options = options || {};

    // Make a request to the API
    request({
      url: 'geographies/' + id + '/media/recent',
      data: options,
      success: callback
    });

  };

  // # Helpers

  // Returns the client-specific authentication URL that is created upon initialization.

  // Parses a given parameter from the browsers hash.
  // Optionally, the parameter can be removed from
  // the URL upon successful matching.

  function hashParam (param, remove) {

    // Create a RegExp object for parsing params
    var regex = new RegExp("(?:&|#)" + param + "=([a-z0-9._-]+)", "i");

    // Look for matches in the windows hash
    var matches = window.location.hash.match(regex);

    // If matches are found...
    if (matches) {

      // ...then remove the parameter if specified
      if (remove) {
        var removeRegex = new RegExp("(?:&|#)" + param + "=" + matches[1], "i");
        window.location.hash = window.location.hash.replace(removeRegex, '');
      }

      // ...and return the first matching param
      return matches[1];

    }

    // Otherwise return false if no matching params are found
    return false;

  }

  // Makes JSONP requests to the Instagram API

  function request (options) {

    var urlBase = 'https://api.instagram.com/v1/',
        callbackName = 'instajam' + Math.round(new Date().getTime() / 1000) + Math.floor(Math.random() * 100);

    options = options || {};
    options.data = options.data || {};
    options.data.access_token = localStorage.getItem('instagram_access_token');
    options.data.callback = callbackName;

    var queryString = serializeParams(options.data);

    if (options.url) {
      options.url = urlBase + options.url + '?' + queryString;

      window[callbackName] = function(data) {

        if (typeof options.success === 'function') {
          options.success(data);
        }

        script.parentNode.removeChild(script);
        delete window[callbackName];
      };

      var script = document.createElement('script');
      script.type = 'text/javascript';
      script.src = options.url;
      document.getElementsByTagName('body')[0].appendChild(script);

    } else {
      throw new InstajamError("Instajam:: Missing request URL");
    }

  }

  // Given a JavaScript object, return a
  // string suitable for passing in a URL

  function serializeParams (obj) {
    var str = [];
    for (var p in obj) {
      str.push(encodeURIComponent(p) + '=' + encodeURIComponent(obj[p]));
    }
    return str.join('&');
  }

  // Define a custom error object

  function InstajamError (message) {
    this.name = "InstajamError";
    this.message = message || '';
  }

  InstajamError.prototype = Error.prototype;

  // Return new instances of the endpoint
  // helpers as top-level keys

  Instajam.user = new User();
  Instajam.media = new Media();
  Instajam.tag = new Tag();
  Instajam.location = new Location();
  // Instajam.geography = new Geography();

}(window.Instajam = window.Instajam || {}));
<|endoftext|>


<commit_before>'use strict';

var _ = require('lodash');

var handlers = {
	RPL_WELCOME: function(command) {
        var nick =  command.params[0];

        // Get the server name so we know which messages are by the server in future
        this.network.server = command.prefix;

        this.network.cap.negotiating = false;
        this.emit('registered', {
            nick: nick
        });
    },

    RPL_ISUPPORT: function(command) {
        var options = command.params;
        var i;
        var option;
        var matches;
        var j;

        for (i = 1; i < options.length; i++) {
            option = options[i].split('=', 2);
            option[0] = option[0].toUpperCase();

            this.network.options[option[0]] = (typeof option[1] !== 'undefined') ? option[1] : true;

            if (option[0] === 'PREFIX') {
                matches = /\(([^)]*)\)(.*)/.exec(option[1]);
                if (matches && matches.length === 3) {
                    this.network.options.PREFIX = [];
                    for (j = 0; j < matches[2].length; j++) {
                        this.network.options.PREFIX.push({
                            symbol: matches[2].charAt(j),
                            mode: matches[1].charAt(j)
                        });
                    }
                }
            } else if (option[0] === 'CHANTYPES') {
                this.network.options.CHANTYPES = this.network.options.CHANTYPES.split('');
            } else if (option[0] === 'CHANMODES') {
                this.network.options.CHANMODES = option[1].split(',');
            } else if (option[0] === 'NETWORK') {
                this.network.name = option[1];
            } else if (option[0] === 'NAMESX' && !this.network.cap.isEnabled('multi-prefix')) {
                // Tell the server to send us all user modes in NAMES reply, not just
                // the highest one
                this.connection.write('PROTOCTL NAMESX');
            }
        }

        this.emit('server options', {
            options: this.network.options,
            cap: this.network.cap.enabled
        });
    },

    CAP: function(command) {
        var request_caps = [];
        var capability_values = Object.create(null);

        // TODO: capability modifiers
        // i.e. - for disable, ~ for requires ACK, = for sticky
        var capabilities = command.params[command.params.length - 1]
            .replace(/(?:^| )[\-~=]/, '')
            .split(' ')
            .map(function(cap) {
                // CAPs in 3.2 may be in the form of CAP=VAL. So seperate those out
                var sep = cap.indexOf('=');
                if (sep === -1) {
                    return cap;
                }

                var cap_name = cap.substr(0, sep);
                var cap_value = cap.substr(sep + 1);

                capability_values[cap_name] = cap_value;
                return cap_name;
            });

        // Which capabilities we want to enable
        var want = [
            'batch',
            'multi-prefix',
            'draft/message-tags-0.2',
            'away-notify',
            'invite-notify',
            'account-notify',
            'account-tag',
            'server-time',
            'userhost-in-names',
            'extended-join',
            'znc.in/server-time-iso',
            'znc.in/server-time',
            'twitch.tv/membership'
        ];

        // Optional CAPs depending on settings
        if (this.connection.options.password) {
            want.push('sasl');
        }
        if (this.connection.options.enable_chghost) {
            want.push('chghost');
        }
        if (this.connection.options.enable_echomessage) {
            want.push('echo-message');
        }

        want = _(want)
            .concat(this.request_extra_caps)
            .uniq()
            .value();

        switch (command.params[1]) {
            case 'LS':
                // Compute which of the available capabilities we want and request them
                request_caps = _.intersection(capabilities, want);
                if (request_caps.length > 0) {
                    this.network.cap.requested = this.network.cap.requested.concat(request_caps);
                }

                // CAP 3.2 multline support. Only send our CAP requests on the last CAP LS
                // line which will not have * set for params[2]
                if (command.params[2] !== '*') {
                    if (request_caps.length > 0) {
                        this.connection.write('CAP REQ :' + request_caps.join(' '));
                    } else {
                        this.connection.write('CAP END');
                        this.network.cap.negotiating = false;
                    }
                }
                break;
            case 'ACK':
                if (capabilities.length > 0) {
                    // Update list of enabled capabilities
                    this.network.cap.enabled = capabilities;
                    // Update list of capabilities we would like to have but that aren't enabled
                    this.network.cap.requested = _.difference(
                        this.network.cap.requested,
                        capabilities
                    );
                }
                if (this.network.cap.enabled.length > 0) {
                    if (this.network.cap.isEnabled('sasl')) {
                        if (this.connection.options.sasl_mechanism === 'AUTHCOOKIE') {
                            this.connection.write('AUTHENTICATE AUTHCOOKIE');
                        } else {
                            this.connection.write('AUTHENTICATE PLAIN');
                        }
                    } else {
                        this.connection.write('CAP END');
                        this.network.cap.negotiating = false;
                    }
                }
                break;
            case 'NAK':
                if (capabilities.length > 0) {
                    this.network.cap.requested = _.difference(
                        this.network.cap.requested,
                        capabilities
                    );
                }
                if (this.network.cap.requested.length > 0) {
                    this.connection.write('CAP END');
                    this.network.cap.negotiating = false;
                }
                break;
            case 'LIST':
                // should we do anything here?
                break;
            case 'NEW':
                // Not supported yet
                break;
            case 'DEL':
                // Not supported yet
                break;
        }
    },

    AUTHENTICATE: function(command) {
        var auth_str = this.connection.options.nick + '\0' +
            this.connection.options.nick + '\0' +
            this.connection.options.password;
        var b = new Buffer(auth_str, 'utf8');
        var b64 = b.toString('base64');

        if (command.params[0] === '+') {
            while (b64.length >= 400) {
                this.connection.write('AUTHENTICATE ' + b64.slice(0, 399));
                b64 = b64.slice(399);
            }
            if (b64.length > 0) {
                this.connection.write('AUTHENTICATE ' + b64);
            } else {
                this.connection.write('AUTHENTICATE +');
            }
        } else {
            this.connection.write('CAP END');
            this.network.cap.negotiating = false;
        }
    },

    RPL_SASLAUTHENTICATED: function() {
        this.connection.write('CAP END');
        this.network.cap.negotiating = false;
    },

    RPL_SASLLOGGEDIN: function() {
        if (this.network.cap.negotiating === true) {
            this.connection.write('CAP END');
            this.network.cap.negotiating = false;
        }
    },

    ERR_SASLNOTAUTHORISED: function() {
        this.connection.write('CAP END');
        this.network.cap.negotiating = false;
    },

    ERR_SASLABORTED: function() {
        this.connection.write('CAP END');
        this.network.cap.negotiating = false;
    },

    ERR_SASLALREADYAUTHED: function() {
        // noop
    }
};

module.exports = function AddCommandHandlers(command_controller) {
    _.each(handlers, function(handler, handler_command) {
        command_controller.addHandler(handler_command, handler);
    });
};
<commit_msg>Remove `twitch.tv/membership` from the default CAP request #128<commit_after>'use strict';

var _ = require('lodash');

var handlers = {
	RPL_WELCOME: function(command) {
        var nick =  command.params[0];

        // Get the server name so we know which messages are by the server in future
        this.network.server = command.prefix;

        this.network.cap.negotiating = false;
        this.emit('registered', {
            nick: nick
        });
    },

    RPL_ISUPPORT: function(command) {
        var options = command.params;
        var i;
        var option;
        var matches;
        var j;

        for (i = 1; i < options.length; i++) {
            option = options[i].split('=', 2);
            option[0] = option[0].toUpperCase();

            this.network.options[option[0]] = (typeof option[1] !== 'undefined') ? option[1] : true;

            if (option[0] === 'PREFIX') {
                matches = /\(([^)]*)\)(.*)/.exec(option[1]);
                if (matches && matches.length === 3) {
                    this.network.options.PREFIX = [];
                    for (j = 0; j < matches[2].length; j++) {
                        this.network.options.PREFIX.push({
                            symbol: matches[2].charAt(j),
                            mode: matches[1].charAt(j)
                        });
                    }
                }
            } else if (option[0] === 'CHANTYPES') {
                this.network.options.CHANTYPES = this.network.options.CHANTYPES.split('');
            } else if (option[0] === 'CHANMODES') {
                this.network.options.CHANMODES = option[1].split(',');
            } else if (option[0] === 'NETWORK') {
                this.network.name = option[1];
            } else if (option[0] === 'NAMESX' && !this.network.cap.isEnabled('multi-prefix')) {
                // Tell the server to send us all user modes in NAMES reply, not just
                // the highest one
                this.connection.write('PROTOCTL NAMESX');
            }
        }

        this.emit('server options', {
            options: this.network.options,
            cap: this.network.cap.enabled
        });
    },

    CAP: function(command) {
        var request_caps = [];
        var capability_values = Object.create(null);

        // TODO: capability modifiers
        // i.e. - for disable, ~ for requires ACK, = for sticky
        var capabilities = command.params[command.params.length - 1]
            .replace(/(?:^| )[\-~=]/, '')
            .split(' ')
            .map(function(cap) {
                // CAPs in 3.2 may be in the form of CAP=VAL. So seperate those out
                var sep = cap.indexOf('=');
                if (sep === -1) {
                    return cap;
                }

                var cap_name = cap.substr(0, sep);
                var cap_value = cap.substr(sep + 1);

                capability_values[cap_name] = cap_value;
                return cap_name;
            });

        // Which capabilities we want to enable
        var want = [
            'batch',
            'multi-prefix',
            'draft/message-tags-0.2',
            'away-notify',
            'invite-notify',
            'account-notify',
            'account-tag',
            'server-time',
            'userhost-in-names',
            'extended-join',
            'znc.in/server-time-iso',
            'znc.in/server-time'
        ];

        // Optional CAPs depending on settings
        if (this.connection.options.password) {
            want.push('sasl');
        }
        if (this.connection.options.enable_chghost) {
            want.push('chghost');
        }
        if (this.connection.options.enable_echomessage) {
            want.push('echo-message');
        }

        want = _(want)
            .concat(this.request_extra_caps)
            .uniq()
            .value();

        switch (command.params[1]) {
            case 'LS':
                // Compute which of the available capabilities we want and request them
                request_caps = _.intersection(capabilities, want);
                if (request_caps.length > 0) {
                    this.network.cap.requested = this.network.cap.requested.concat(request_caps);
                }

                // CAP 3.2 multline support. Only send our CAP requests on the last CAP LS
                // line which will not have * set for params[2]
                if (command.params[2] !== '*') {
                    if (request_caps.length > 0) {
                        this.connection.write('CAP REQ :' + request_caps.join(' '));
                    } else {
                        this.connection.write('CAP END');
                        this.network.cap.negotiating = false;
                    }
                }
                break;
            case 'ACK':
                if (capabilities.length > 0) {
                    // Update list of enabled capabilities
                    this.network.cap.enabled = capabilities;
                    // Update list of capabilities we would like to have but that aren't enabled
                    this.network.cap.requested = _.difference(
                        this.network.cap.requested,
                        capabilities
                    );
                }
                if (this.network.cap.enabled.length > 0) {
                    if (this.network.cap.isEnabled('sasl')) {
                        if (this.connection.options.sasl_mechanism === 'AUTHCOOKIE') {
                            this.connection.write('AUTHENTICATE AUTHCOOKIE');
                        } else {
                            this.connection.write('AUTHENTICATE PLAIN');
                        }
                    } else {
                        this.connection.write('CAP END');
                        this.network.cap.negotiating = false;
                    }
                }
                break;
            case 'NAK':
                if (capabilities.length > 0) {
                    this.network.cap.requested = _.difference(
                        this.network.cap.requested,
                        capabilities
                    );
                }
                if (this.network.cap.requested.length > 0) {
                    this.connection.write('CAP END');
                    this.network.cap.negotiating = false;
                }
                break;
            case 'LIST':
                // should we do anything here?
                break;
            case 'NEW':
                // Not supported yet
                break;
            case 'DEL':
                // Not supported yet
                break;
        }
    },

    AUTHENTICATE: function(command) {
        var auth_str = this.connection.options.nick + '\0' +
            this.connection.options.nick + '\0' +
            this.connection.options.password;
        var b = new Buffer(auth_str, 'utf8');
        var b64 = b.toString('base64');

        if (command.params[0] === '+') {
            while (b64.length >= 400) {
                this.connection.write('AUTHENTICATE ' + b64.slice(0, 399));
                b64 = b64.slice(399);
            }
            if (b64.length > 0) {
                this.connection.write('AUTHENTICATE ' + b64);
            } else {
                this.connection.write('AUTHENTICATE +');
            }
        } else {
            this.connection.write('CAP END');
            this.network.cap.negotiating = false;
        }
    },

    RPL_SASLAUTHENTICATED: function() {
        this.connection.write('CAP END');
        this.network.cap.negotiating = false;
    },

    RPL_SASLLOGGEDIN: function() {
        if (this.network.cap.negotiating === true) {
            this.connection.write('CAP END');
            this.network.cap.negotiating = false;
        }
    },

    ERR_SASLNOTAUTHORISED: function() {
        this.connection.write('CAP END');
        this.network.cap.negotiating = false;
    },

    ERR_SASLABORTED: function() {
        this.connection.write('CAP END');
        this.network.cap.negotiating = false;
    },

    ERR_SASLALREADYAUTHED: function() {
        // noop
    }
};

module.exports = function AddCommandHandlers(command_controller) {
    _.each(handlers, function(handler, handler_command) {
        command_controller.addHandler(handler_command, handler);
    });
};
<|endoftext|>