<commit_before>var httpStatusCodes = require('http').STATUS_CODES,
    request = require('request'),
    /*xml2js = require('xml2js'),
    xmlParser = new xml2js.Parser({explicitArray: false}),*/
    xmlParser = new require('./parser')(),
    extend = require('extend');

module.exports = function(config) {

  this.request = function(route, data, options, cb) {
    if (typeof data == 'function') {
      cb = data;
      data = null;
      options = {};
    } else if (typeof options == 'function') {
      cb = options;
      options = {};
    }

    var self = this;
        url = route[0],
        method = route[1],
        defaultOptions = {
      url: config.subdomain + url,
      method: method,
      headers: {
        Accept: 'application/xml'
        //'Content-Type': 'application/xml; charset=utf-8'
      },
      auth: {
        user: config.apiKey,
        pass: '',
        sendImmediately: true
      },
      body: data
    };

    options = extend(true, defaultOptions, options);

    //console.log('Options: ', options);

    request(options, function(e, res, body) {
      if (e) return cb(e);
      //console.log('isXML: ', res.headers);
      if (options.headers.Accept == 'application/xml') {
        xmlParser.parseXML(body, function(e, parsedXml) {
          if (config.debug) self._debug(res, parsedXml);
          var apiErrors = self._parseError(res, parsedXml);
          if (apiErrors.length) return cb(apiErrors);
          cb(null, self._wrapResponse(res, parsedXml));
        });
      } else {
        if (config.debug) self._debug(res, body);
        var apiErrors = self._parseError(res, body);
        if (apiErrors.length) return cb(apiErrors);
        cb(null, self._wrapResponse(res, body));
      }
    });
  };

  this._wrapResponse = function(res, body) {
    return {
      statusCode: res.statusCode,
      headers: res.headers,
      body: body
    };
  };

  this._parseError = function(res, body) {
    var errors = [],
        newError = null;
    console.log('BODY: ', body);
    if (body && body.errors && body.errors.error) {
      if (body.errors.error instanceof Array) {
        for (var i = 0; i < body.errors.length; i++) {
          var currentError = body.errors[i];
          newError = new Error(currentError.$.field + ' ' + currentError._);
          newError.body = currentError;
          newError.code = currentError.$.symbol;
          errors.push(newError);
        }
      } else {
        var currentError = body.errors.error;
        newError = new Error(currentError.$.field + ' ' + currentError._);
        newError.body = currentError;
        newError.code = currentError.$.symbol;
        errors.push(newError);
      }
    } else if (res.statusCode >= 400 && res.statusCode <= 499) { // Client Request Error
      if (body) newError = new Error('Client Request Error: ' + JSON.stringify(body));
      else newError = new Error('Client Request Error: ' + httpStatusCodes[res.statusCode]);
      newError.body = body;
      newError.code = res.statusCode;
      errors.push(newError);
    } else if (res.statusCode >= 500 && res.statusCode <= 599) { // Server Error
      if (body) newError = new Error('Server Error: ' + JSON.stringify(body));
      else newError = new Error('Server Error: ' + httpStatusCodes[res.statusCode]);
      newError.body = body;
      newError.code = res.statusCode;
      errors.push(newError);
    }
    return errors;
  };

  this._debug = function(res, body) { console.log('DEBUG: ', res.statusCode, ' \n', body); };
};<commit_msg>improved invoice calls<commit_after>var httpStatusCodes = require('http').STATUS_CODES,
    request = require('request'),
    /*xml2js = require('xml2js'),
    xmlParser = new xml2js.Parser({explicitArray: false}),*/
    xmlParser = require('./parser'),
    xmlParser = new xmlParser(),
    extend = require('extend');

module.exports = function(config) {

  this.request = function(route, data, options, cb) {
    if (typeof data == 'function') {
      cb = data;
      data = null;
      options = {};
    } else if (typeof options == 'function') {
      cb = options;
      options = {};
    }

    var self = this;
        url = route[0],
        method = route[1],
        defaultOptions = {
      url: config.subdomain + url,
      method: method,
      headers: {
        Accept: 'application/xml'
        //'Content-Type': 'application/xml; charset=utf-8'
      },
      auth: {
        user: config.apiKey,
        pass: '',
        sendImmediately: true
      },
      body: data
    };

    options = extend(true, defaultOptions, options);

    //console.log('Options: ', options);

    request(options, function(e, res, body) {
      if (e) return cb(e);
      //console.log('isXML: ', res.headers);
      if (options.headers.Accept == 'application/xml') {
        xmlParser.parseXML(body, function(e, parsedXml) {
          if (config.debug) self._debug(res, parsedXml);
          var apiErrors = self._parseError(res, parsedXml);
          if (apiErrors.length) return cb(apiErrors);
          cb(null, self._wrapResponse(res, parsedXml));
        });
      } else {
        if (config.debug) self._debug(res, body);
        var apiErrors = self._parseError(res, body);
        if (apiErrors.length) return cb(apiErrors);
        cb(null, self._wrapResponse(res, body));
      }
    });
  };

  this._wrapResponse = function(res, body) {
    return {
      statusCode: res.statusCode,
      headers: res.headers,
      body: body
    };
  };

  this._parseError = function(res, body) {
    var errors = [],
        newError = null;
    console.log('BODY: ', body);
    if (body && body.errors && body.errors.error) {
      if (body.errors.error instanceof Array) {
        for (var i = 0; i < body.errors.length; i++) {
          var currentError = body.errors[i];
          newError = new Error(currentError.$.field + ' ' + currentError._);
          newError.body = currentError;
          newError.code = currentError.$.symbol;
          errors.push(newError);
        }
      } else {
        var currentError = body.errors.error;
        newError = new Error(currentError.$.field + ' ' + currentError._);
        newError.body = currentError;
        newError.code = currentError.$.symbol;
        errors.push(newError);
      }
    } else if (res.statusCode >= 400 && res.statusCode <= 499) { // Client Request Error
      if (body) newError = new Error('Client Request Error: ' + JSON.stringify(body));
      else newError = new Error('Client Request Error: ' + httpStatusCodes[res.statusCode]);
      newError.body = body;
      newError.code = res.statusCode;
      errors.push(newError);
    } else if (res.statusCode >= 500 && res.statusCode <= 599) { // Server Error
      if (body) newError = new Error('Server Error: ' + JSON.stringify(body));
      else newError = new Error('Server Error: ' + httpStatusCodes[res.statusCode]);
      newError.body = body;
      newError.code = res.statusCode;
      errors.push(newError);
    }
    return errors;
  };

  this._debug = function(res, body) { console.log('DEBUG: ', res.statusCode, ' \n', body); };
};<|endoftext|>


<commit_before>var util          = require('util'),
    events        = require('events'),
    extend        = require('node.extend'),
    EventStream   = require('./event_stream'),
    Logger        = require('./logger'),
    Validations   = require('./client/Validations'),
    Binder        = require('./client/event_source_bindings'),
    logSubscriber = require('./log_subscribers/client_subscriber');

function Client(es, options, req, res) {
  events.EventEmitter.call(this);

  this.req   = req;
  this.res   = res;
  this.queue = null;
  this.ctag  = null;
  this.options = options || {};

  if(this.req.params.userId) {
    this.userId = this.req.params.userId;
  } else if(this.req.query.userId) {
    this.userId = this.req.query.userId;
  } else {
    this.userId = this.req.sessionID;
  }
  // Generating a random number ensures that each window/client gets its own
  // unique queue. Otherwise, queues shared between windows would get messages
  // delivered in a round-robin manner and would thus miss messages.
  this.id = process.hrtime()[1];

  this.stream = new EventStream(this);

  this.logger = new Logger('[TownCrier:'+this.userId+':'+this.id+']');

  // Watch for memory leaks?
  var logSub = new logSubscriber(this.logger);
  logSub.attachTo(this);
  logSub.attachTo(this.stream);

  Binder.call(this, es);
  Validations.call(this);
}

function unsubscribe(fanout, queue, ctag) {
  queue.unsubscribe(ctag);

  this.queue.removeListener('message', this._onMessage);

  this.queue = null;
  this.ctag  = null;

  this.emit('unsubscribe', this, queue, ctag);
}

function subscribe(fanout, queue, ctag) {
  this.queue = queue;
  this.ctag  = ctag;

  this.queue.on('message', this._onMessage);

  this.emit('subscribe', this, fanout, queue, ctag);
}

function openStream(eventsource) {
  this.stream.open();
}

function destroy() {
  this.emit('destroy', this);

  return true;
}

// Declare the public API
util.inherits(Client, events.EventEmitter);
extend(Client.prototype, {
  subscribe    : subscribe,
  unsubscribe  : unsubscribe,
  openStream   : openStream,
  destroy      : destroy
});

module.exports = Client;<commit_msg>Fixes a bug when Client#unsubscribe is called before it is bound to<commit_after>var util          = require('util'),
    events        = require('events'),
    extend        = require('node.extend'),
    EventStream   = require('./event_stream'),
    Logger        = require('./logger'),
    Validations   = require('./client/Validations'),
    Binder        = require('./client/event_source_bindings'),
    logSubscriber = require('./log_subscribers/client_subscriber');

function Client(es, options, req, res) {
  events.EventEmitter.call(this);

  this.req   = req;
  this.res   = res;
  this.queue = null;
  this.ctag  = null;
  this.options = options || {};

  if(this.req.params.userId) {
    this.userId = this.req.params.userId;
  } else if(this.req.query.userId) {
    this.userId = this.req.query.userId;
  } else {
    this.userId = this.req.sessionID;
  }
  // Generating a random number ensures that each window/client gets its own
  // unique queue. Otherwise, queues shared between windows would get messages
  // delivered in a round-robin manner and would thus miss messages.
  this.id = process.hrtime()[1];

  this.stream = new EventStream(this);

  this.logger = new Logger('[TownCrier:'+this.userId+':'+this.id+']');

  // Watch for memory leaks?
  var logSub = new logSubscriber(this.logger);
  logSub.attachTo(this);
  logSub.attachTo(this.stream);

  Binder.call(this, es);
  Validations.call(this);
}

function unsubscribe(fanout, queue, ctag) {
  queue.unsubscribe(ctag);

  if(this.queue) this.queue.removeListener('message', this._onMessage);

  this.queue = null;
  this.ctag  = null;

  this.emit('unsubscribe', this, queue, ctag);
}

function subscribe(fanout, queue, ctag) {
  this.queue = queue;
  this.ctag  = ctag;

  this.queue.on('message', this._onMessage);

  this.emit('subscribe', this, fanout, queue, ctag);
}

function openStream(eventsource) {
  this.stream.open();
}

function destroy() {
  this.emit('destroy', this);

  return true;
}

// Declare the public API
util.inherits(Client, events.EventEmitter);
extend(Client.prototype, {
  subscribe    : subscribe,
  unsubscribe  : unsubscribe,
  openStream   : openStream,
  destroy      : destroy
});

module.exports = Client;<|endoftext|>


<commit_before>var EventEmitter = require('events').EventEmitter;
var net = require('net');
var Query = require(__dirname+'/query');
var Parser = require(__dirname+'/parser');

var Client = function(config) {
  EventEmitter.call(this);
  config = config || {};
  this.user = config.user;
  this.database = config.database;
  this.port = config.port || 5432;
  this.host = config.host;
  this.queryQueue = [];
  this.stream = config.stream || new net.Stream();
  this.queryQueue = [];
};

sys.inherits(Client, EventEmitter);

Client.prototype.connect = function() {
  if(this.stream.readyState == 'closed'){
    this.stream.connect(this.port, this.host);
  }
  var self = this;
  this.stream.on('connect', function() {
    var data = ['user',self.user,'database', self.database, '\0'].join('\0');
    var dataBuffer = Buffer(data);
    var fullBuffer = Buffer(8 + dataBuffer.length);
    fullBuffer[0] = fullBuffer.length >>> 24;
    fullBuffer[1] = fullBuffer.length >>> 16;
    fullBuffer[2] = fullBuffer.length >>> 8;
    fullBuffer[3] = fullBuffer.length >>> 0;
    fullBuffer[4] = 0;
    fullBuffer[5] = 3;
    fullBuffer[6] = 0;
    fullBuffer[7] = 0;
    fullBuffer.write(data,8);
    self.stream.write(fullBuffer);
  });

  this.stream.on('data', function(data) {
    var parser = new Parser(data);
    var msg = parser.parseMessage();
    while(msg) {
      self.emit('message', msg);
      self.emit(msg.name, msg);
      msg = parser.parseMessage();
    }
  });

  this.on('ReadyForQuery', function() {
    self.readyForQuery = true;
    self.pulseQueryQueue();
  });
};

Client.prototype.disconnect = function() {
  var terminationBuffer = new Buffer([58,0,0,0,4]);
  this.stream.write(terminationBuffer);
};

Client.prototype.query = function(text) {
  var query = new Query();
  query.client = this;
  query.text = text;
  this.queryQueue.push(query);
  this.pulseQueryQueue();
  return query;
};

Client.prototype.pulseQueryQueue = function() {
  if(!this.readyForQuery) {
    return;
  };
  var query = this.queryQueue.shift();
  if(query) {
    var self = this;
    this.readyForQuery = false;
    this.stream.write(query.toBuffer());
    var rowHandler = function(msg) {
      query.emit('row',msg.fields)
    };
    var endHandler;
    endHandler = function(msg) {
      query.emit('end');
      self.removeListener('CommandComplete', endHandler);
      self.removeListener('DataRow', rowHandler);
    };
    this.on('DataRow', rowHandler);
    this.on('CommandComplete', endHandler);
  }
};

module.exports = Client;
<commit_msg>code cleanup<commit_after>var EventEmitter = require('events').EventEmitter;
var net = require('net');
var Query = require(__dirname+'/query');
var Parser = require(__dirname+'/parser');

var Client = function(config) {
  EventEmitter.call(this);
  config = config || {};
  this.user = config.user;
  this.database = config.database;
  this.port = config.port || 5432;
  this.host = config.host;
  this.queryQueue = [];
  this.stream = config.stream || new net.Stream();
  this.queryQueue = [];
};

sys.inherits(Client, EventEmitter);

var p = Client.prototype;

p.connect = function() {
  if(this.stream.readyState == 'closed'){
    this.stream.connect(this.port, this.host);
  }
  var self = this;
  this.stream.on('connect', function() {
    var data = ['user',self.user,'database', self.database, '\0'].join('\0');
    var dataBuffer = Buffer(data);
    var fullBuffer = Buffer(8 + dataBuffer.length);
    fullBuffer[0] = fullBuffer.length >>> 24;
    fullBuffer[1] = fullBuffer.length >>> 16;
    fullBuffer[2] = fullBuffer.length >>> 8;
    fullBuffer[3] = fullBuffer.length >>> 0;
    fullBuffer[4] = 0;
    fullBuffer[5] = 3;
    fullBuffer[6] = 0;
    fullBuffer[7] = 0;
    fullBuffer.write(data,8);
    self.stream.write(fullBuffer);
  });

  this.stream.on('data', function(data) {
    var parser = new Parser(data);
    var msg = parser.parseMessage();
    while(msg) {
      self.emit('message', msg);
      self.emit(msg.name, msg);
      msg = parser.parseMessage();
    }
  });

  this.on('ReadyForQuery', function() {
    self.readyForQuery = true;
    self.pulseQueryQueue();
  });
};

p.disconnect = function() {
  var terminationBuffer = new Buffer([58,0,0,0,4]);
  this.stream.write(terminationBuffer);
};

p.query = function(text) {
  var query = new Query();
  query.client = this;
  query.text = text;
  this.queryQueue.push(query);
  this.pulseQueryQueue();
  return query;
};

p.pulseQueryQueue = function() {
  if(!this.readyForQuery) {
    return;
  };
  var query = this.queryQueue.shift();
  if(query) {
    var self = this;
    this.readyForQuery = false;
    this.stream.write(query.toBuffer());
    var rowHandler = function(msg) {
      query.emit('row',msg.fields)
    };
    var endHandler;
    endHandler = function(msg) {
      query.emit('end');
      self.removeListener('CommandComplete', endHandler);
      self.removeListener('DataRow', rowHandler);
    };
    this.on('DataRow', rowHandler);
    this.on('CommandComplete', endHandler);
  }
};

module.exports = Client;
<|endoftext|>


<commit_before>'use strict'

var mqtt = require('mqtt-packet')
var EE = require('events').EventEmitter
var util = require('util')
var eos = require('end-of-stream')
var empty = new Buffer(0)
var Packet = require('aedes-packet')
var write = require('./write')
var QoSPacket = require('./qos-packet')
var handleSubscribe = require('./handlers/subscribe')
var handleUnsubscribe = require('./handlers/unsubscribe')
var handle = require('./handlers')

module.exports = Client

function Client (broker, conn) {
  var that = this

  this.broker = broker
  this.conn = conn
  this.parser = mqtt.parser()
  this.connected = false
  this.errored = false
  this.clean = true
  this._handling = 0
  this.subscriptions = {}
  this.id = null

  conn.client = this

  this.parser.client = this

  this._parsingBatch = 1
  this._nextId = Math.ceil(Math.random() * 65535)

  this.disconnected = false

  this.parser.on('packet', enqueue)

  function nextBatch (err) {
    if (err) {
      return that.emit('error', err)
    }

    var buf = empty
    var client = that

    that._parsingBatch--
    if (that._parsingBatch <= 0) {
      that._parsingBatch = 0
      buf = client.conn.read(null)

      if (buf) {
        client.parser.parse(buf)
      }
    }
  }
  this._nextBatch = nextBatch

  nextBatch()

  conn.on('readable', nextBatch)
  conn.on('error', this.emit.bind(this, 'error'))
  this.parser.on('error', this.emit.bind(this, 'error'))

  this.on('error', onError)

  this._eos = eos(this.conn, this.close.bind(this))

  this.deliver0 = function deliverQoS0 (_packet, cb) {
    var packet = new Packet(_packet, broker)
    packet.qos = 0
    write(that, packet, cb)
  }

  this.deliverQoS = function deliverQoS (_packet, cb) {
    var packet = new QoSPacket(_packet, that)
    packet.writeCallback = cb
    broker.persistence.outgoingUpdate(that, packet, writeQoS)
  }

  this._keepaliveTimer = null
  this._keepaliveInterval = -1

  this._connectTimer = setTimeout(function () {
    that.emit('error', new Error('connect did not arrive in time'))
  }, broker.connectTimeout)
}

function writeQoS (err, client, packet) {
  if (err) {
    // is this right, or we should ignore thins?
    client.emit('error', err)
  } else {
    write(client, packet, packet.writeCallback)
  }
}

function drainRequest (req) {
  req.callback()
}

function onError (err) {
  this.errored = true
  this.conn.removeAllListeners('error')
  this.conn.on('error', nop)
  this.broker.emit('clientError', this, err)
  this.close()
}

util.inherits(Client, EE)

Client.prototype._onError = onError

Client.prototype.publish = function (message, done) {
  var packet = new Packet(message, this.broker)
  var that = this
  if (packet.qos === 0) {
    // skip offline and send it as it is
    this.deliver0(packet, done || nop)
  } else if (!this.clean && this.id) {
    this.broker.persistence.outgoingEnqueue({
      clientId: this.id
    }, packet, function deliver (err) {
      if (err) {
        return done(err)
      }
      that.deliverQoS(packet, done)
    })
  } else {
    that.deliverQoS(packet, done)
  }
}

Client.prototype.subscribe = function (packet, done) {
  if (!packet.subscriptions) {
    if (!Array.isArray(packet)) {
      packet = [packet]
    }
    packet = {
      subscriptions: packet
    }
  }
  handleSubscribe(this, packet, done)
}

Client.prototype.close = function (done) {
  var that = this
  var conn = this.conn

  if (this.connected) {
    handleUnsubscribe(
      this,
      { unsubscriptions: Object.keys(this.subscriptions) },
      finish)
  } else {
    finish()
  }

  this.parser.removeAllListeners('packet')
  conn.removeAllListeners('readable')

  if (this._keepaliveTimer) {
    this._keepaliveTimer.clear()
    this._keepaliveInterval = -1
    this._keepaliveTimer = null
  }

  if (this._connectTimer) {
    clearTimeout(this._connectTimer)
    this._connectTimer = null
  }

  this._eos()
  this._eos = nop

  function finish () {
    if (!that.disconnected && that.will) {
      that.broker.publish(that.will)
    }

    conn.removeAllListeners('error')
    conn.on('error', nop)

    if (that.broker.clients[that.id]) {
      that.broker.unregisterClient(that)
    }

    // hack to clean up the write callbacks
    // supports streams2 & streams3, so node 0.10, 0.11, and iojs
    var state = that.conn._writableState
    var list = state.getBuffer && state.getBuffer() || state.buffer
    list.forEach(drainRequest)

    if (conn.destroy) {
      conn.destroy()
    } else {
      conn.end()
    }

    if (typeof done === 'function') {
      done()
    }
  }
}

function enqueue (packet) {
  this.client._parsingBatch++
  handle(this.client, packet, this.client._nextBatch)
}

function nop () {}
<commit_msg>Add QoS downgrade on publish.<commit_after>'use strict'

var mqtt = require('mqtt-packet')
var EE = require('events').EventEmitter
var util = require('util')
var eos = require('end-of-stream')
var empty = new Buffer(0)
var Packet = require('aedes-packet')
var write = require('./write')
var QoSPacket = require('./qos-packet')
var handleSubscribe = require('./handlers/subscribe')
var handleUnsubscribe = require('./handlers/unsubscribe')
var handle = require('./handlers')

module.exports = Client

function Client (broker, conn) {
  var that = this

  this.broker = broker
  this.conn = conn
  this.parser = mqtt.parser()
  this.connected = false
  this.errored = false
  this.clean = true
  this._handling = 0
  this.subscriptions = {}
  this.id = null

  conn.client = this

  this.parser.client = this

  this._parsingBatch = 1
  this._nextId = Math.ceil(Math.random() * 65535)

  this.disconnected = false

  this.parser.on('packet', enqueue)

  function nextBatch (err) {
    if (err) {
      return that.emit('error', err)
    }

    var buf = empty
    var client = that

    that._parsingBatch--
    if (that._parsingBatch <= 0) {
      that._parsingBatch = 0
      buf = client.conn.read(null)

      if (buf) {
        client.parser.parse(buf)
      }
    }
  }
  this._nextBatch = nextBatch

  nextBatch()

  conn.on('readable', nextBatch)
  conn.on('error', this.emit.bind(this, 'error'))
  this.parser.on('error', this.emit.bind(this, 'error'))

  this.on('error', onError)

  this._eos = eos(this.conn, this.close.bind(this))

  this.deliver0 = function deliverQoS0 (_packet, cb) {
    var packet = new Packet(_packet, broker)
    packet.qos = 0
    write(that, packet, cb)
  }

  this.deliverQoS = function deliverQoS (_packet, cb) {
    //TODO: downgrade to QoS1 if applicable
    if (_packet.qos == 0){
      that.deliver0(_packet, cb);
    } else {
      var packet = new QoSPacket(_packet, that)
      packet.writeCallback = cb
      broker.persistence.outgoingUpdate(that, packet, writeQoS)
    }
  }

  this._keepaliveTimer = null
  this._keepaliveInterval = -1

  this._connectTimer = setTimeout(function () {
    that.emit('error', new Error('connect did not arrive in time'))
  }, broker.connectTimeout)
}

function writeQoS (err, client, packet) {
  if (err) {
    // is this right, or we should ignore thins?
    client.emit('error', err)
  } else {
    write(client, packet, packet.writeCallback)
  }
}

function drainRequest (req) {
  req.callback()
}

function onError (err) {
  this.errored = true
  this.conn.removeAllListeners('error')
  this.conn.on('error', nop)
  this.broker.emit('clientError', this, err)
  this.close()
}

util.inherits(Client, EE)

Client.prototype._onError = onError

Client.prototype.publish = function (message, done) {
  var packet = new Packet(message, this.broker)
  var that = this
  if (packet.qos === 0) {
    // skip offline and send it as it is
    this.deliver0(packet, done || nop)
  } else if (!this.clean && this.id) {
    this.broker.persistence.outgoingEnqueue({
      clientId: this.id
    }, packet, function deliver (err) {
      if (err) {
        return done(err)
      }
      that.deliverQoS(packet, done)
    })
  } else {
    that.deliverQoS(packet, done)
  }
}

Client.prototype.subscribe = function (packet, done) {
  if (!packet.subscriptions) {
    if (!Array.isArray(packet)) {
      packet = [packet]
    }
    packet = {
      subscriptions: packet
    }
  }
  handleSubscribe(this, packet, done)
}

Client.prototype.close = function (done) {
  var that = this
  var conn = this.conn

  if (this.connected) {
    handleUnsubscribe(
      this,
      { unsubscriptions: Object.keys(this.subscriptions) },
      finish)
  } else {
    finish()
  }

  this.parser.removeAllListeners('packet')
  conn.removeAllListeners('readable')

  if (this._keepaliveTimer) {
    this._keepaliveTimer.clear()
    this._keepaliveInterval = -1
    this._keepaliveTimer = null
  }

  if (this._connectTimer) {
    clearTimeout(this._connectTimer)
    this._connectTimer = null
  }

  this._eos()
  this._eos = nop

  function finish () {
    if (!that.disconnected && that.will) {
      that.broker.publish(that.will)
    }

    conn.removeAllListeners('error')
    conn.on('error', nop)

    if (that.broker.clients[that.id]) {
      that.broker.unregisterClient(that)
    }

    // hack to clean up the write callbacks
    // supports streams2 & streams3, so node 0.10, 0.11, and iojs
    var state = that.conn._writableState
    var list = state.getBuffer && state.getBuffer() || state.buffer
    list.forEach(drainRequest)

    if (conn.destroy) {
      conn.destroy()
    } else {
      conn.end()
    }

    if (typeof done === 'function') {
      done()
    }
  }
}

function enqueue (packet) {
  this.client._parsingBatch++
  handle(this.client, packet, this.client._nextBatch)
}

function nop () {}
<|endoftext|>


<commit_before>function ddg_spice_google_plus(google) {
	console.log(google);
	var out = '';
	if(google.kind === "plus#peopleFeed" && google.items.length > 0) {
		var tmp, div, div2, link, img, item, limit;

		//Sometimes, the API returns a lot of results even if we
		//asked for only five. (e.g. coke)
		if(google['items'].length > 5) {
			limit = 5;
		} else {
			limit = google['items'].length;
		}

		for (var i = 0;i < limit;i++) {
		    item = google.items[i];

		    div = d.createElement("div");
		    div2 = d.createElement("div");

		    link = d.createElement("a");
		    link.href = '/?q=google%2B+userid:' + item.id;

		 //    if (item.displayName.length >= 10) {
			// 	item.displayName = item.displayName.substring(0,8) + "...";
			// }

		    img = d.createElement('img');
		    img.src = item.image.url;
		    YAHOO.util.Dom.setStyle(img, "margin", '0 auto 0 auto');
		    YAHOO.util.Dom.setStyle(div,'margin-bottom', '10px');
		    YAHOO.util.Dom.setStyle(div,'text-align', 'center');
		    link.appendChild(img);
		    div.appendChild(link);

		    link = d.createElement('a');
		    link.href = '/?q=google%2B+userid:' + item.id;
		    link.innerHTML = item.displayName;
		    div.appendChild(link);
		    div.appendChild(d.createElement('br'));

		    YAHOO.util.Dom.addClass(div, 'inline highlight_zero_click1 highlight_zero_click_wrapper');
		    YAHOO.util.Dom.setStyle(div, "float", "left");
		    YAHOO.util.Dom.setStyle(div, "margin", "10px 20px 10px 0px");
		    YAHOO.util.Dom.setStyle(div, "padding", "5px");
		    YAHOO.util.Dom.setStyle(div, "max-width", "80px");

		    div2.appendChild(div);

		    out += div2.innerHTML;
		}

		var items = new Array();
		items[0] = new Array();
		items[0]['a'] = out += '

';
		var query = DDG.get_query(); //"google+ this is a test";
		var query = query.replace(/\s*(google\+|google\splus|g\+|gplus|\+)\s*/, "");
		items[0]['h'] = 'Google+ Users (' + query + ')';
		items[0]['s'] = 'Google+';
		items[0]['f'] = 1;
		items[0]['u'] = 'http://plus.google.com/s/' + query;
		nra(items,1,1);
	} else if(google.kind === "plus#person") {
		//Check if the user has a tagline
		//Tagline or the about me page can be displayed here.
		if (google.tagline) {
			out += '
Introduction:  ' + google.tagline + '
';
		} else if(google.aboutMe) {
			out += '
Introduction:  ' + google.aboutMe.substring(0, 200) + '...
';
		}

		//Check for organizations
		if(google.organizations) {
			var orgs = '';
			for(var i=0;i < google.organizations.length && i < 2;i++) {
				orgs += google.organizations[i].name +
					(google.organizations[i].title ? ' (' + google.organizations[i].title + ')' : '');
				if(i !== google.organizations.length-1) {
					orgs += ', ';
				}
			}
			out += '
Organizations: ' + orgs + '
';
		}
		//Check if the person has links to show.
		if(google.urls) {
			var links = '';
			if(google.urls.length > 2) {
				google.urls.length -= 2;
				for(var i=0;i < google.urls.length;i++) {
					var re = /(?:https?:\/\/)?(?:www\.)?([^\/]+).*/;
					var string =  google.urls[i].value;
					string = string.replace(re, "$1");
					re = /\.com/;
					string = string.replace(re, "");
					if(string.search(/\./) === -1) {
						string = string.charAt(0).toUpperCase() + string.slice(1)
					}
					links += '' + string
						+ '';
					if(i !== google.urls.length-1) {
						links += ', ';
					}
				}
				out += '
Links: ' + links + '
';
			}
		}
		var items = new Array();
		items[0] = new Array();
		items[0]['a'] = out += '

';
		items[0]['h'] = google.displayName;
		items[0]['s'] = 'Google+';
		items[0]['u'] = google.url;
		items[0]['f'] = 1;
		items[0]['i'] =	google.image.url.substring(0, google.image.url.length-6);
		nra(items);
	}
}
<commit_msg>Changed the links to lowercase<commit_after>function ddg_spice_google_plus(google) {
	console.log(google);
	var out = '';
	if(google.kind === "plus#peopleFeed" && google.items.length > 0) {
		var tmp, div, div2, link, img, item, limit;

		//Sometimes, the API returns a lot of results even if we
		//asked for only five. (e.g. coke)
		if(google['items'].length > 5) {
			limit = 5;
		} else {
			limit = google['items'].length;
		}

		for (var i = 0;i < limit;i++) {
		    item = google.items[i];

		    div = d.createElement("div");
		    div2 = d.createElement("div");

		    link = d.createElement("a");
		    link.href = '/?q=google%2B+userid:' + item.id;

		 //    if (item.displayName.length >= 10) {
			// 	item.displayName = item.displayName.substring(0,8) + "...";
			// }

		    img = d.createElement('img');
		    img.src = item.image.url;
		    YAHOO.util.Dom.setStyle(img, "margin", '0 auto 0 auto');
		    YAHOO.util.Dom.setStyle(div,'margin-bottom', '10px');
		    YAHOO.util.Dom.setStyle(div,'text-align', 'center');
		    link.appendChild(img);
		    div.appendChild(link);

		    link = d.createElement('a');
		    link.href = '/?q=google%2B+userid:' + item.id;
		    link.innerHTML = item.displayName;
		    div.appendChild(link);
		    div.appendChild(d.createElement('br'));

		    YAHOO.util.Dom.addClass(div, 'inline highlight_zero_click1 highlight_zero_click_wrapper');
		    YAHOO.util.Dom.setStyle(div, "float", "left");
		    YAHOO.util.Dom.setStyle(div, "margin", "10px 20px 10px 0px");
		    YAHOO.util.Dom.setStyle(div, "padding", "5px");
		    YAHOO.util.Dom.setStyle(div, "max-width", "80px");

		    div2.appendChild(div);

		    out += div2.innerHTML;
		}

		var items = new Array();
		items[0] = new Array();
		items[0]['a'] = out += '

';
		var query = DDG.get_query(); //"google+ this is a test";
		var query = query.replace(/\s*(google\+|google\splus|g\+|gplus|\+)\s*/, "");
		items[0]['h'] = 'Google+ Users (' + query + ')';
		items[0]['s'] = 'Google+';
		items[0]['f'] = 1;
		items[0]['u'] = 'http://plus.google.com/s/' + query;
		nra(items,1,1);
	} else if(google.kind === "plus#person") {
		//Check if the user has a tagline
		//Tagline or the about me page can be displayed here.
		if (google.tagline) {
			out += '
Introduction:  ' + google.tagline + '
';
		} else if(google.aboutMe) {
			out += '
Introduction:  ' + google.aboutMe.substring(0, 200) + '...
';
		}

		//Check for organizations
		if(google.organizations) {
			var orgs = '';
			for(var i=0;i < google.organizations.length && i < 2;i++) {
				orgs += google.organizations[i].name +
					(google.organizations[i].title ? ' (' + google.organizations[i].title + ')' : '');
				if(i !== google.organizations.length-1) {
					orgs += ', ';
				}
			}
			out += '
Organizations: ' + orgs + '
';
		}
		//Check if the person has links to show.
		if(google.urls) {
			var links = '';
			if(google.urls.length > 2) {
				google.urls.length -= 2;
				for(var i=0;i < google.urls.length;i++) {
					var re = /(?:https?:\/\/)?(?:www\.)?([^\/]+).*/;
					var string =  google.urls[i].value.toLowerCase();
					string = string.replace(re, "$1");
					re = /\.com/;
					string = string.replace(re, "");
					if(string.search(/\./) === -1) {
						string = string.charAt(0).toUpperCase() + string.slice(1)
					}
					links += '' + string
						+ '';
					if(i !== google.urls.length-1) {
						links += ', ';
					}
				}
				out += '
Links: ' + links + '
';
			}
		}
		var items = new Array();
		items[0] = new Array();
		items[0]['a'] = out += '

';
		items[0]['h'] = google.displayName;
		items[0]['s'] = 'Google+';
		items[0]['u'] = google.url;
		items[0]['f'] = 1;
		items[0]['i'] =	google.image.url.substring(0, google.image.url.length-6);
		nra(items);
	}
}
<|endoftext|>


<commit_before>// @flow
import * as React from 'react'
import {Box2} from './box'
import Icon, {castPlatformStyles as iconCastPlatformStyles} from './icon'
import Text from './text'
import * as Styles from '../styles'

type Color = 'blue' | 'red' | 'yellow' | 'green' | 'grey'

type Props = {
  actions?: Array<{
    title: string,
    onClick: () => void,
  }>,
  color: Color,
  onClose?: () => void,
  text: string,
}

const Banner = (props: Props) => (
  <Box2
    direction="horizontal"
    fullWidth={true}
    style={Styles.collapseStyles([styles.container, colorToBackgroundColorStyles[props.color]])}
  >
    <Box2 key="textBox" direction="horizontal" style={styles.textContainer} centerChildren={true}>
      <Text
        type="BodySmallSemibold"
        style={Styles.collapseStyles([styles.text, colorToTextColorStyles[props.color]])}
      >
        {props.text}
        {!!props.actions &&
          props.actions.reduce(
            (parts, {title, onClick}, index) => [
              ...parts,
              <Text key={`space-${index}`} type="BodySmallSemibold">

              </Text>,
              <Text
                key={`action-${index}`}
                type="BodySmallSemibold"
                onClick={onClick}
                style={colorToTextColorStyles[props.color]}
                underline={true}
              >
                {title}
              </Text>,
            ],
            []
          )}
      </Text>
    </Box2>
    {!!props.onClose && (
      <Box2 key="iconBox" direction="vertical" style={styles.iconContainer} centerChildren={true}>
        <Icon
          fontSize={Styles.isMobile ? undefined : Styles.globalMargins.xsmall}
          type="iconfont-close"
          style={iconCastPlatformStyles(styles.icon)}
          color={Styles.globalColors.white_90}
          hoverColor={Styles.globalColors.white}
          onClick={props.onClose}
        />
      </Box2>
    )}
  </Box2>
)

const styles = Styles.styleSheetCreate({
  container: {
    minHeight: Styles.globalMargins.large,
  },
  icon: {
    padding: Styles.globalMargins.tiny,
  },
  iconContainer: {
    alignSelf: 'flex-start',
    height: Styles.globalMargins.large,
  },
  text: {
    textAlign: 'center',
  },
  textContainer: Styles.platformStyles({
    common: {
      flex: 1,
      paddingBottom: Styles.globalMargins.tiny,
      paddingTop: Styles.globalMargins.tiny,
    },
    isElectron: {
      paddingLeft: Styles.globalMargins.xlarge,
      paddingRight: Styles.globalMargins.xlarge,
    },
    isMobile: {
      paddingLeft: Styles.globalMargins.medium,
      paddingRight: Styles.globalMargins.medium,
    },
  }),
})

const colorToBackgroundColorStyles = {
  blue: {backgroundColor: Styles.globalColors.blue},
  green: {backgroundColor: Styles.globalColors.green},
  grey: {backgroundColor: Styles.globalColors.lightGrey2},
  red: {backgroundColor: Styles.globalColors.red},
  yellow: {backgroundColor: Styles.globalColors.yellow},
}

const colorToTextColorStyles = {
  blue: {color: Styles.globalColors.white},
  green: {color: Styles.globalColors.white},
  grey: {color: Styles.globalColors.black_50},
  red: {color: Styles.globalColors.white},
  yellow: {color: Styles.globalColors.brown_75},
}

export default Banner
<commit_msg>capture the native sweetness by using styleSheetCreate<commit_after>// @flow
import * as React from 'react'
import {Box2} from './box'
import Icon, {castPlatformStyles as iconCastPlatformStyles} from './icon'
import Text from './text'
import * as Styles from '../styles'

type Color = 'blue' | 'red' | 'yellow' | 'green' | 'grey'

type Props = {
  actions?: Array<{
    title: string,
    onClick: () => void,
  }>,
  color: Color,
  onClose?: () => void,
  text: string,
}

const Banner = (props: Props) => (
  <Box2
    direction="horizontal"
    fullWidth={true}
    style={Styles.collapseStyles([styles.container, colorToBackgroundColorStyles[props.color]])}
  >
    <Box2 key="textBox" direction="horizontal" style={styles.textContainer} centerChildren={true}>
      <Text
        type="BodySmallSemibold"
        style={Styles.collapseStyles([styles.text, colorToTextColorStyles[props.color]])}
      >
        {props.text}
        {!!props.actions &&
          props.actions.reduce(
            (parts, {title, onClick}, index) => [
              ...parts,
              <Text key={`space-${index}`} type="BodySmallSemibold">

              </Text>,
              <Text
                key={`action-${index}`}
                type="BodySmallSemibold"
                onClick={onClick}
                style={colorToTextColorStyles[props.color]}
                underline={true}
              >
                {title}
              </Text>,
            ],
            []
          )}
      </Text>
    </Box2>
    {!!props.onClose && (
      <Box2 key="iconBox" direction="vertical" style={styles.iconContainer} centerChildren={true}>
        <Icon
          fontSize={Styles.isMobile ? undefined : Styles.globalMargins.xsmall}
          type="iconfont-close"
          style={iconCastPlatformStyles(styles.icon)}
          color={Styles.globalColors.white_90}
          hoverColor={Styles.globalColors.white}
          onClick={props.onClose}
        />
      </Box2>
    )}
  </Box2>
)

const styles = Styles.styleSheetCreate({
  container: {
    minHeight: Styles.globalMargins.large,
  },
  icon: {
    padding: Styles.globalMargins.tiny,
  },
  iconContainer: {
    alignSelf: 'flex-start',
    height: Styles.globalMargins.large,
  },
  text: {
    textAlign: 'center',
  },
  textContainer: Styles.platformStyles({
    common: {
      flex: 1,
      paddingBottom: Styles.globalMargins.tiny,
      paddingTop: Styles.globalMargins.tiny,
    },
    isElectron: {
      paddingLeft: Styles.globalMargins.xlarge,
      paddingRight: Styles.globalMargins.xlarge,
    },
    isMobile: {
      paddingLeft: Styles.globalMargins.medium,
      paddingRight: Styles.globalMargins.medium,
    },
  }),
})

const colorToBackgroundColorStyles = Styles.styleSheetCreate({
  blue: {backgroundColor: Styles.globalColors.blue},
  green: {backgroundColor: Styles.globalColors.green},
  grey: {backgroundColor: Styles.globalColors.lightGrey2},
  red: {backgroundColor: Styles.globalColors.red},
  yellow: {backgroundColor: Styles.globalColors.yellow},
})

const colorToTextColorStyles = Styles.styleSheetCreate({
  blue: {color: Styles.globalColors.white},
  green: {color: Styles.globalColors.white},
  grey: {color: Styles.globalColors.black_50},
  red: {color: Styles.globalColors.white},
  yellow: {color: Styles.globalColors.brown_75},
})

export default Banner
<|endoftext|>


<commit_before>/*!
 * Copyright 2014 Apereo Foundation (AF) Licensed under the
 * Educational Community License, Version 2.0 (the "License"); you may
 * not use this file except in compliance with the License. You may
 * obtain a copy of the License at
 *
 *     http://opensource.org/licenses/ECL-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an "AS IS"
 * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */

define(['exports', 'jquery'], function(exports, $) {

    /**
     * Get a full folder profile
     *
     * @param  {String}         folderId                Id of the folder we're trying to retrieve
     * @param  {Function}       callback                Standard callback function
     * @param  {Object}         callback.err            Error object containing error code and error message
     * @param  {Folder}         callback.folder         Folder object representing the retrieved folder
     * @throws {Error}                                  Error thrown when no folder id has been provided
     */
    var getFolder = exports.getFolder = function(folderId, callback) {
        if (!folderId) {
            throw new Error('A valid folder id should be provided');
        }

        $.ajax({
            'url': '/api/folder/' + folderId,
            'success': function(data) {
                callback(null, data);
            },
            'error': function(jqXHR, textStatus) {
                callback({'code': jqXHR.status, 'msg': jqXHR.responseText});
            }
        });
    };

    /**
     * Create a new folder
     *
     * @param  {String}         displayName             Display title for the created folder
     * @param  {String}         [description]           The folder's description
     * @param  {String}         [visibility]            The folder's visibility. This can be public, loggedin or private
     * @param  {String[]}       [managers]              Array of user/group ids that should be added as managers to the folder
     * @param  {String[]}       [viewers]               Array of user/group ids that should be added as viewers to the folder
     * @param  {Function}       [callback]              Standard callback function
     * @param  {Object}         [callback.err]          Error object containing error code and error message
     * @param  {Folder}         [callback.folder]       Folder object representing the created folder
     * @throws {Error}                                  Error thrown when no valid display name has been provided
     */
    var createFolder = exports.createFolder = function(displayName, description, visibility, managers, viewers, callback) {
        if (!displayName) {
            throw new Error('A valid folder name should be provided');
        }

        // Set a default callback function in case no callback function has been provided
        callback = callback || function() {};

        var data = {
            'displayName': displayName,
            'description': description,
            'visibility': visibility,
            'managers': managers,
            'viewers': viewers
        };

        $.ajax({
            'url': '/api/folder',
            'type': 'POST',
            'data': data,
            'success': function(data) {
                callback(null, data);
            },
            'error': function(jqXHR, textStatus) {
                callback({'code': jqXHR.status, 'msg': jqXHR.responseText});
            }
        });
    };

    /**
     * Update a folder's metadata
     *
     * @param  {String}       folderId                  Id of the folder we're trying to update
     * @param  {Object}       params                    JSON object where the keys represent all of the profile field names we want to update and the values represent the new values for those fields
     * @param  {Function}     [callback]                Standard callback function
     * @param  {Object}       [callback.err]            Error object containing error code and error message
     * @param  {Folder}       [callback.folder]         Folder object representing the updated folder
     * @throws {Error}                                  Error thrown when not all of the required parameters have been provided
     */
    var updateFolder = exports.updateFolder = function(folderId, params, callback) {
        if (!folderId) {
            throw new Error('A valid folder id should be provided');
        } else if (!params || _.keys(params).length === 0) {
            throw new Error('At least one update parameter should be provided');
        }

        // Set a default callback function in case no callback function has been provided
        callback = callback || function() {};

        $.ajax({
            'url': '/api/folder/' + folderId,
            'type': 'POST',
            'data': params,
            'success': function(data) {
                callback(null, data.folder);
            },
            'error': function(jqXHR, textStatus) {
                callback({'code': jqXHR.status, 'msg': jqXHR.responseText});
            }
        });
    };

    /**
     * Get the viewers and managers of a folder
     *
     * @param  {String}          folderId                       Id of the folder we're trying to retrieve the members for
     * @param  {String}          [start]                        The token used for paging. If the first page of results is required, `null` should be passed in as the token. For any subsequent pages, the `nextToken` provided in the feed from the previous page should be used
     * @param  {Number}          [limit]                        The number of members to retrieve
     * @param  {Function}        callback                       Standard callback function
     * @param  {Object}          callback.err                   Error object containing error code and error message
     * @param  {Object}          callback.members               Response object containing the folders members and nextToken
     * @param  {User[]|Group[]}  callback.members.results       Array that contains an object for each member. Each object has a role property that contains the role of the member and a profile property that contains the principal profile of the member
     * @param  {String}          callback.members.nextToken     The value to provide in the `start` parameter to get the next set of results
     * @throws {Error}                                          Error thrown when no folder id has been provided
     */
    var getMembers = exports.getMembers = function(folderId, start, limit, callback) {
        if (!folderId) {
            throw new Error('A valid folder id should be provided');
        }

        var data = {
            'start': start,
            'limit': limit
        };

        $.ajax({
            'url': '/api/folder/'+ folderId + '/members',
            'data': data,
            'success': function(data) {
                callback(null, data);
            },
            'error': function(jqXHR, textStatus) {
                callback({'code': jqXHR.status, 'msg': jqXHR.responseText});
            }
        });
    };

    /**
     * Change the members and managers of a folder
     *
     * @param  {String}       folderId              Id of the folder we're trying to update the members for
     * @param  {Object}       updatedMembers        JSON Object where the keys are the user/group ids we want to update membership for, and the values are the roles these members should get (manager or viewer). If false is passed in as a role, the principal will be removed as a member
     * @param  {Function}     [callback]            Standard callback function
     * @param  {Object}       [callback.err]        Error object containing error code and error message
     * @throws {Error}                              Error thrown when not all of the required parameters have been provided
     */
    var updateMembers = exports.updateMembers = function(folderId, updatedMembers, callback) {
        if (!folderId) {
            throw new Error('A valid folder id should be provided');
        } else if (!updatedMembers || _.keys(updatedMembers).length === 0) {
            throw new Error('The updatedMembers hash should contain at least 1 update');
        }

        // Set a default callback function in case no callback function has been provided
        callback = callback || function() {};

        $.ajax({
            'url': '/api/folder/'+ folderId + '/members',
            'type': 'POST',
            'data': updatedMembers,
            'success': function() {
                callback(null);
            },
            'error': function(jqXHR, textStatus) {
                callback({'code': jqXHR.status, 'msg': jqXHR.responseText});
            }
        });
    };

    /**
     * Share a folder
     *
     * @param  {String}       folderId              Id of the folder we're trying to share
     * @param  {String[]}     principals            Array of principal ids with who the folder should be shared
     * @param  {Function}     [callback]            Standard callback function
     * @param  {Object}       [callback.err]        Error object containing error code and error message
     * @throws {Error}                              Error thrown when not all of the required parameters have been provided
     */
    var shareFolder = exports.shareFolder = function(folderId, principals, callback) {
        if (!folderId) {
            throw new Error('A folder ID should be provided');
        } else if (!principals.length) {
            throw new Error('A user or group to share with should be provided');
        }

        // Set a default callback function in case no callback function has been provided
        callback = callback || function() {};

        var data = {
            'viewers': principals
        };

        $.ajax({
            'url': '/api/folder/' + folderId + '/share',
            'type': 'POST',
            'data': data,
            'success': function(data) {
                callback(null, data);
            },
            'error': function(jqXHR, textStatus) {
                callback({'code': jqXHR.status, 'msg': jqXHR.responseText});
            }
        });
    };

});
<commit_msg>Added API function for getting the folder library<commit_after>/*!
 * Copyright 2014 Apereo Foundation (AF) Licensed under the
 * Educational Community License, Version 2.0 (the "License"); you may
 * not use this file except in compliance with the License. You may
 * obtain a copy of the License at
 *
 *     http://opensource.org/licenses/ECL-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an "AS IS"
 * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 * or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */

define(['exports', 'jquery'], function(exports, $) {

    /**
     * Get a full folder profile
     *
     * @param  {String}         folderId                Id of the folder we're trying to retrieve
     * @param  {Function}       callback                Standard callback function
     * @param  {Object}         callback.err            Error object containing error code and error message
     * @param  {Folder}         callback.folder         Folder object representing the retrieved folder
     * @throws {Error}                                  Error thrown when no folder id has been provided
     */
    var getFolder = exports.getFolder = function(folderId, callback) {
        if (!folderId) {
            throw new Error('A valid folder id should be provided');
        }

        $.ajax({
            'url': '/api/folder/' + folderId,
            'success': function(data) {
                callback(null, data);
            },
            'error': function(jqXHR, textStatus) {
                callback({'code': jqXHR.status, 'msg': jqXHR.responseText});
            }
        });
    };

    /**
     * Create a new folder
     *
     * @param  {String}         displayName             Display title for the created folder
     * @param  {String}         [description]           The folder's description
     * @param  {String}         [visibility]            The folder's visibility. This can be public, loggedin or private
     * @param  {String[]}       [managers]              Array of user/group ids that should be added as managers to the folder
     * @param  {String[]}       [viewers]               Array of user/group ids that should be added as viewers to the folder
     * @param  {Function}       [callback]              Standard callback function
     * @param  {Object}         [callback.err]          Error object containing error code and error message
     * @param  {Folder}         [callback.folder]       Folder object representing the created folder
     * @throws {Error}                                  Error thrown when no valid display name has been provided
     */
    var createFolder = exports.createFolder = function(displayName, description, visibility, managers, viewers, callback) {
        if (!displayName) {
            throw new Error('A valid folder name should be provided');
        }

        // Set a default callback function in case no callback function has been provided
        callback = callback || function() {};

        var data = {
            'displayName': displayName,
            'description': description,
            'visibility': visibility,
            'managers': managers,
            'viewers': viewers
        };

        $.ajax({
            'url': '/api/folder',
            'type': 'POST',
            'data': data,
            'success': function(data) {
                callback(null, data);
            },
            'error': function(jqXHR, textStatus) {
                callback({'code': jqXHR.status, 'msg': jqXHR.responseText});
            }
        });
    };

    /**
     * Update a folder's metadata
     *
     * @param  {String}       folderId                  Id of the folder we're trying to update
     * @param  {Object}       params                    JSON object where the keys represent all of the profile field names we want to update and the values represent the new values for those fields
     * @param  {Function}     [callback]                Standard callback function
     * @param  {Object}       [callback.err]            Error object containing error code and error message
     * @param  {Folder}       [callback.folder]         Folder object representing the updated folder
     * @throws {Error}                                  Error thrown when not all of the required parameters have been provided
     */
    var updateFolder = exports.updateFolder = function(folderId, params, callback) {
        if (!folderId) {
            throw new Error('A valid folder id should be provided');
        } else if (!params || _.keys(params).length === 0) {
            throw new Error('At least one update parameter should be provided');
        }

        // Set a default callback function in case no callback function has been provided
        callback = callback || function() {};

        $.ajax({
            'url': '/api/folder/' + folderId,
            'type': 'POST',
            'data': params,
            'success': function(data) {
                callback(null, data.folder);
            },
            'error': function(jqXHR, textStatus) {
                callback({'code': jqXHR.status, 'msg': jqXHR.responseText});
            }
        });
    };

    /**
     * Get the viewers and managers of a folder
     *
     * @param  {String}          folderId                       Id of the folder we're trying to retrieve the members for
     * @param  {String}          [start]                        The token used for paging. If the first page of results is required, `null` should be passed in as the token. For any subsequent pages, the `nextToken` provided in the feed from the previous page should be used
     * @param  {Number}          [limit]                        The number of members to retrieve
     * @param  {Function}        callback                       Standard callback function
     * @param  {Object}          callback.err                   Error object containing error code and error message
     * @param  {Object}          callback.members               Response object containing the folders members and nextToken
     * @param  {User[]|Group[]}  callback.members.results       Array that contains an object for each member. Each object has a role property that contains the role of the member and a profile property that contains the principal profile of the member
     * @param  {String}          callback.members.nextToken     The value to provide in the `start` parameter to get the next set of results
     * @throws {Error}                                          Error thrown when no folder id has been provided
     */
    var getMembers = exports.getMembers = function(folderId, start, limit, callback) {
        if (!folderId) {
            throw new Error('A valid folder id should be provided');
        }

        var data = {
            'start': start,
            'limit': limit
        };

        $.ajax({
            'url': '/api/folder/'+ folderId + '/members',
            'data': data,
            'success': function(data) {
                callback(null, data);
            },
            'error': function(jqXHR, textStatus) {
                callback({'code': jqXHR.status, 'msg': jqXHR.responseText});
            }
        });
    };

    /**
     * Change the members and managers of a folder
     *
     * @param  {String}       folderId              Id of the folder we're trying to update the members for
     * @param  {Object}       updatedMembers        JSON Object where the keys are the user/group ids we want to update membership for, and the values are the roles these members should get (manager or viewer). If false is passed in as a role, the principal will be removed as a member
     * @param  {Function}     [callback]            Standard callback function
     * @param  {Object}       [callback.err]        Error object containing error code and error message
     * @throws {Error}                              Error thrown when not all of the required parameters have been provided
     */
    var updateMembers = exports.updateMembers = function(folderId, updatedMembers, callback) {
        if (!folderId) {
            throw new Error('A valid folder id should be provided');
        } else if (!updatedMembers || _.keys(updatedMembers).length === 0) {
            throw new Error('The updatedMembers hash should contain at least 1 update');
        }

        // Set a default callback function in case no callback function has been provided
        callback = callback || function() {};

        $.ajax({
            'url': '/api/folder/'+ folderId + '/members',
            'type': 'POST',
            'data': updatedMembers,
            'success': function() {
                callback(null);
            },
            'error': function(jqXHR, textStatus) {
                callback({'code': jqXHR.status, 'msg': jqXHR.responseText});
            }
        });
    };

    /**
     * Share a folder
     *
     * @param  {String}       folderId              Id of the folder we're trying to share
     * @param  {String[]}     principals            Array of principal ids with who the folder should be shared
     * @param  {Function}     [callback]            Standard callback function
     * @param  {Object}       [callback.err]        Error object containing error code and error message
     * @throws {Error}                              Error thrown when not all of the required parameters have been provided
     */
    var shareFolder = exports.shareFolder = function(folderId, principals, callback) {
        if (!folderId) {
            throw new Error('A folder ID should be provided');
        } else if (!principals.length) {
            throw new Error('A user or group to share with should be provided');
        }

        // Set a default callback function in case no callback function has been provided
        callback = callback || function() {};

        var data = {
            'viewers': principals
        };

        $.ajax({
            'url': '/api/folder/' + folderId + '/share',
            'type': 'POST',
            'data': data,
            'success': function(data) {
                callback(null, data);
            },
            'error': function(jqXHR, textStatus) {
                callback({'code': jqXHR.status, 'msg': jqXHR.responseText});
            }
        });
    };

    /**
     * Get the folder library for a given principal
     *
     * @param  {String}         principalId                     User or group id for who we want to retrieve the folder library
     * @param  {String}         [start]                         The token used for paging. If the first page of results is required, `null` should be passed in as the token. For any subsequent pages, the `nextToken` provided in the feed from the previous page should be used
     * @param  {Number}         [limit]                         The number of folders to retrieve
     * @param  {Function}       callback                        Standard callback function
     * @param  {Object}         callback.err                    Error object containing error code and error message
     * @param  {Object}         callback.folders                Response object containing the folders in the requested library and nextToken
     * @param  {Folder[]}       callback.folders.results        Array of folders representing the folders present in the library
     * @param  {String}         callback.folders.nextToken      The value to provide in the `start` parameter to get the next set of results
     * @throws {Error}                                          Error thrown when no principal ID has been provided
     */
    var getLibrary = exports.getLibrary = function(principalId, start, limit, callback) {
        if (!principalId) {
            throw new Error('A user or group ID should be provided');
        }

        var data = {
            'start': start,
            'limit': limit
        };

        $.ajax({
            'url': '/api/folder/library/' + principalId,
            'data': data,
            'success': function(data) {
                callback(null, data);
            },
            'error': function(jqXHR, textStatus) {
                callback({'code': jqXHR.status, 'msg': jqXHR.responseText});
            }
        });
    };

});
<|endoftext|>


<commit_before>var PROMISE_TIMEOUT = 5000, // 5 seconds
    ZAF_EVENT       = /^zaf\./,
    version         = require('version'),
    Promise         = window.Promise || require('../vendor/native-promise-only'),
    when            = require('utils').when,
    pendingPromises = {},
    ids             = {};

function nextIdFor(name) {
  if (isNaN(ids[name])) {
    ids[name] = 0;
  }
  return ++ids[name];
}

function rawPostMessage(client, msg, forceReady) {
  if (client.ready || forceReady) {
    client._source.postMessage(msg, client._origin);
  } else {
    client.on('app.registered', rawPostMessage.bind(null, client, msg));
  }
}

// params is an array for get, and an object for set/invoke
function wrappedPostMessage(name, params) {
  var id = nextIdFor('promise'),
      timeoutId;

  var promise = new Promise(function(resolve, reject) {
    // Time out the promise to ensure it will be garbage collected if nobody responds
    timeoutId = setTimeout(function() {
      reject(new Error('Invocation request timeout'));
    }, PROMISE_TIMEOUT);

    pendingPromises[id] = {resolve: resolve, reject: reject};

    var msg = JSON.stringify({ id: id, request: name, params: params, appGuid: this._appGuid });
    rawPostMessage(this, msg);
  }.bind(this));

  // ensure promise is cleaned up when resolved
  return promise.then(removePromise.bind(null, id, timeoutId), removePromise.bind(null, id, timeoutId));
}

function removePromise(id, timeoutId, args) {
  clearTimeout(timeoutId);
  delete pendingPromises[id];
  if (args instanceof Error) throw args;
  return args;
}

function isValidEvent(client, event) {
  return client && client._origin === event.origin && client._source === event.source;
}

function messageHandler(client, event) {
  if (!isValidEvent(client, event)) { return; }

  var data = event.data;

  if (!data) { return; }

  if (typeof data === 'string') {
    try {
      data = JSON.parse(event.data);
    } catch (e) {
      return e;
    }
  }

  var pendingPromise;
  if (data.id && (pendingPromise = pendingPromises[data.id])) {
    if (data.error) {
      var err = data.error;
      if (err.code) {
        err = new Error(data.error.msg);
        err.name = data.error.code;
        err.stack = data.error.stack;
      }
      pendingPromise.reject(err);
    } else {
      pendingPromise.resolve(data.result);
    }
  } else if (ZAF_EVENT.test(data.key)) {
    var key = data.key.replace(ZAF_EVENT, ''),
        msg = { appGuid: client._appGuid };
    if (data.needsReply) {
      if (client._repliesPending[key]) { return; }
      msg.key = 'iframe.reply:' + key;
      client._repliesPending[key] = true;
      when(client._messageHandlers[key]).then(
        rawPostMessage.bind(null, client, msg)
      ).catch(function(reason) {
        msg.error = {
          msg: reason
        };
        rawPostMessage(client, msg);
      }).then(function() {
        delete client._repliesPending[key];
      });
    } else {
      client.trigger(key, data.message);
    }
  }
}

var Client = function(origin, appGuid) {
  this._origin = origin;
  this._source = window.parent;
  this._appGuid = appGuid;
  this._messageHandlers = {};
  this._repliesPending = {};
  this._metadata = null;
  this._context = null;
  this.ready = false;

  this.on('app.registered', function(data) {
    this.ready = true;
    this._metadata = data.metadata;
    this._context = data.context;
  }, this);

  this.on('context.updated', function(context) {
    this._context = context;
  }, this);

  this.postMessage('iframe.handshake', { version: version });

  window.addEventListener('message', messageHandler.bind(null, this));
};

Client.prototype = {

  /// ### Client Object
  ///
  /// #### client.postMessage(name, [data])
  ///
  /// Allows you to send message events to the Zendesk app.
  ///
  /// ##### Arguments
  ///
  ///   * `name` the name of the message event. This determines the name of the iframe
  ///     event your app will receive. For example, if you set this to 'hello', your app will receive
  ///     the event 'iframe.hello'
  ///   * `data` (optional) a JSON object with any data that you want to pass along with the
  ///     event
  ///
  /// ```javascript
  /// var client = ZAFClient.init();
  /// client.postMessage('hello', { awesome: true });
  /// ```
  postMessage: function(name, data) {
    var msg = JSON.stringify({ key: name, message: data, appGuid: this._appGuid });
    rawPostMessage(this, msg, name === 'iframe.handshake');
  },

  /// #### client.on(name, handler, [context])
  ///
  /// Allows you to add handlers to a framework event. You can add as many handler as you wish.
  /// They will be executed in the order they were added.
  ///
  /// ##### Arguments
  ///
  ///   * `name` the name of the framework event you want to listen to. This can be
  ///     [framework](./events.html#framework-events), [request](./events.html#request-events), or
  ///     [custom](./events.html#custom-events) events. Your iframe can listen to any events your app
  ///     receives, apart from DOM events. You don't need to register these events in the app first
  ///   * `handler` a function to be called when this event fires. You can expect to receive the same
  ///     event object your app would receive, parsed as JSON
  ///   * `context` (optional) the value of `this` within your handler
  ///
  /// ```javascript
  /// var client = ZAFClient.init();
  /// client.on('app.registered', function(e) {
  ///   // go nuts
  /// });
  /// ```
  ///
  /// Note: As soon as communication with the Zendesk app is established, the SDK triggers an
  /// `app.registered` event. You can add as many handlers to `app.registered` as you like. They're
  /// called immediately after the `init` callback.
  on: function(name, handler, context) {
    if (typeof handler == 'function') {
      handler = context ?
        handler.bind(context) :
        handler;

      this._messageHandlers[name] = this._messageHandlers[name] || [];
      this._messageHandlers[name].push(handler);

      if (name !== 'app.registered') {
        this.postMessage('iframe.on:' + name);
      }
    }
  },

  /// #### client.off(name, handler)
  ///
  /// Allows you to remove a handler for a framework event.
  ///
  /// ##### Arguments
  ///
  ///   * `name` the name of the event
  ///   * `handler` the function you attached earlier with `on`
  ///
  /// ```javascript
  /// var client = ZAFClient.init();
  ///
  /// client.on('app.registered', function appRegistered(e) {
  ///   // do stuff then remove the handler
  ///   client.off('app.registered', appRegistered);
  /// });
  /// ```
  off: function(name, handler) {
    if (!this._messageHandlers[name]) { return false; }
    var index = this._messageHandlers[name].indexOf(handler);
    this.postMessage('iframe.off:' + name);
    return this._messageHandlers[name].splice(index, 1)[0];
  },

  /// #### client.has(name, handler)
  ///
  /// Returns whether or not an event has the specified handler attached to it.
  ///
  /// ##### Arguments
  ///
  ///   * `name` the name of the event
  ///   * `handler` the handler you want to test
  ///
  /// ```javascript
  /// var client = ZAFClient.init();
  ///
  /// client.on('app.registered', function appRegistered(e) {
  ///   // do stuff
  /// });
  ///
  /// client.has('app.registered', appRegistered); // true
  /// client.has('app.activated', appRegistered); // false
  /// ```
  has: function(name, handler) {
    if (!this._messageHandlers[name]) { return false; }
    return this._messageHandlers[name].indexOf(handler) !== -1;
  },

  /// #### client.trigger(name, [data])
  ///
  /// Triggers the specified event on the client.
  ///
  /// ##### Arguments
  ///
  ///   * `name` the name of the event you want to trigger
  ///   * `data` (optional) data you want to pass to the handler
  ///
  /// ```javascript
  /// var client = ZAFClient.init();
  ///
  /// client.on('activation', function {
  ///   console.log('activating!')
  /// });
  ///
  /// client.trigger('activation') // activating!
  /// ```
  trigger: function(name, data) {
    if (!this._messageHandlers[name]) { return false; }
    this._messageHandlers[name].forEach(function(handler) {
      handler(data);
    });
  },

  /// #### client.request(options)
  ///
  /// Dispatch [requests](./requests) via the Zendesk app.
  ///
  /// ##### Arguments
  ///
  ///   * `options` the url of the request or an options object containing a url key/value
  ///
  /// ##### Returns
  ///
  /// A [Promises/A+](https://promisesaplus.com) conformant `promise` object.
  ///
  /// ```javascript
  /// var client = ZAFClient.init();
  ///
  /// client.request('/api/v2/tickets.json').then(
  ///   function(tickets) {
  ///     console.log(tickets);
  ///   },
  ///   function(response) {
  ///     console.error(response.responseText);
  ///   }
  /// );
  /// ```
  request: function(options) {
    var requestKey = 'request:' + nextIdFor('request');

    return new Promise(function(resolve, reject) {
      if (typeof options === 'string') {
        options = { url: options };
      }

      this.on(requestKey + '.done', function(evt) {
        resolve.apply(this, evt.responseArgs);
      });

      this.on(requestKey + '.fail', function(evt) {
        reject.apply(this, evt.responseArgs);
      });

      this.postMessage(requestKey, options);
    }.bind(this));
  },

  metadata: function() {
    return new Promise(function(resolve) {
      if (this._metadata) {
        resolve(this._metadata);
      } else {
        this.on('app.registered', function() {
          resolve(this._metadata);
        }.bind(this));
      }
    }.bind(this));
  },

  context: function() {
    return new Promise(function(resolve) {
      if (this._context) {
        resolve(this._context);
      } else {
        this.on('app.registered', function() {
          resolve(this._context);
        }.bind(this));
      }
    }.bind(this));
  },

  // Accepts string or array of strings.
  get: function(path) {
    var paths = Array.isArray(path) ? path : [path];

    if (paths.some(function(s) {return typeof s !== 'string'; })) {
      throw new Error('The get method accepts a string or array of strings.');
    }

    return wrappedPostMessage.call(this, 'get', paths);
  },

  set: function(key, val) {
    var obj = key;

    if (typeof key === 'string') {
      if (arguments.length === 1) {
        throw new Error('The setter requires a value');
      }
      obj = {};
      obj[key] = val;
    }

    if (typeof obj !== 'object' || Array.isArray(obj)) {
      throw new Error('The set method accepts 2 string, or an object.');
    }

    return wrappedPostMessage.call(this, 'set', obj);
  },

  invoke: function(key) {
    var obj = key;

    if (typeof key === 'string') {
      obj = {};
      obj[key] = Array.prototype.slice.call(arguments, 1);
    } else {
      throw new Error('Invoke with an object isn\'t supported.');
    }

    return wrappedPostMessage.call(this, 'invoke', obj);
  }
};

module.exports = Client;
<commit_msg>Document internal methods<commit_after>var PROMISE_TIMEOUT = 5000, // 5 seconds
    ZAF_EVENT       = /^zaf\./,
    version         = require('version'),
    Promise         = window.Promise || require('../vendor/native-promise-only'),
    when            = require('utils').when,
    pendingPromises = {},
    ids             = {};

function nextIdFor(name) {
  if (isNaN(ids[name])) {
    ids[name] = 0;
  }
  return ++ids[name];
}

// @internal
// #### rawPostMessage(client, msg, forceReady)
//
// Post a message to the hosting frame.
// If the client is not ready and forceReady is not specified, it will wait for client registration.
//
function rawPostMessage(client, msg, forceReady) {
  if (client.ready || forceReady) {
    client._source.postMessage(msg, client._origin);
  } else {
    client.on('app.registered', rawPostMessage.bind(null, client, msg));
  }
}

// @internal
// #### wrappedPostMessage(name, params)
//
// Wraps post message with a request/response mechanism using Promises.
// Must be invoked in the context of a Client.
//
// ##### Arguments
//
//   * `name` the name of the request to make (get/set/invoke)
//   * `data` parameters of the request (an array for get, and an object for set/invoke)
//
function wrappedPostMessage(name, params) {
  var id = nextIdFor('promise'),
      timeoutId;

  var promise = new Promise(function(resolve, reject) {
    // Time out the promise to ensure it will be garbage collected if nobody responds
    timeoutId = setTimeout(function() {
      reject(new Error('Invocation request timeout'));
    }, PROMISE_TIMEOUT);

    pendingPromises[id] = {resolve: resolve, reject: reject};

    var msg = JSON.stringify({ id: id, request: name, params: params, appGuid: this._appGuid });
    rawPostMessage(this, msg);
  }.bind(this));

  // ensure promise is cleaned up when resolved
  return promise.then(removePromise.bind(null, id, timeoutId), removePromise.bind(null, id, timeoutId));
}

function removePromise(id, timeoutId, args) {
  clearTimeout(timeoutId);
  delete pendingPromises[id];
  if (args instanceof Error) throw args;
  return args;
}

function isValidEvent(client, event) {
  return client && client._origin === event.origin && client._source === event.source;
}

function messageHandler(client, event) {
  if (!isValidEvent(client, event)) { return; }

  var data = event.data;

  if (!data) { return; }

  if (typeof data === 'string') {
    try {
      data = JSON.parse(event.data);
    } catch (e) {
      return e;
    }
  }

  var pendingPromise;
  if (data.id && (pendingPromise = pendingPromises[data.id])) {
    if (data.error) {
      var err = data.error;
      if (err.code) {
        err = new Error(data.error.msg);
        err.name = data.error.code;
        err.stack = data.error.stack;
      }
      pendingPromise.reject(err);
    } else {
      pendingPromise.resolve(data.result);
    }
  } else if (ZAF_EVENT.test(data.key)) {
    var key = data.key.replace(ZAF_EVENT, ''),
        msg = { appGuid: client._appGuid };
    if (data.needsReply) {
      if (client._repliesPending[key]) { return; }
      msg.key = 'iframe.reply:' + key;
      client._repliesPending[key] = true;
      when(client._messageHandlers[key]).then(
        rawPostMessage.bind(null, client, msg)
      ).catch(function(reason) {
        msg.error = {
          msg: reason
        };
        rawPostMessage(client, msg);
      }).then(function() {
        delete client._repliesPending[key];
      });
    } else {
      client.trigger(key, data.message);
    }
  }
}

var Client = function(origin, appGuid) {
  this._origin = origin;
  this._source = window.parent;
  this._appGuid = appGuid;
  this._messageHandlers = {};
  this._repliesPending = {};
  this._metadata = null;
  this._context = null;
  this.ready = false;

  this.on('app.registered', function(data) {
    this.ready = true;
    this._metadata = data.metadata;
    this._context = data.context;
  }, this);

  this.on('context.updated', function(context) {
    this._context = context;
  }, this);

  this.postMessage('iframe.handshake', { version: version });

  window.addEventListener('message', messageHandler.bind(null, this));
};

Client.prototype = {

  /// ### Client Object
  ///
  /// #### client.postMessage(name, [data])
  ///
  /// Allows you to send message events to the Zendesk app.
  ///
  /// ##### Arguments
  ///
  ///   * `name` the name of the message event. This determines the name of the iframe
  ///     event your app will receive. For example, if you set this to 'hello', your app will receive
  ///     the event 'iframe.hello'
  ///   * `data` (optional) a JSON object with any data that you want to pass along with the
  ///     event
  ///
  /// ```javascript
  /// var client = ZAFClient.init();
  /// client.postMessage('hello', { awesome: true });
  /// ```
  postMessage: function(name, data) {
    var msg = JSON.stringify({ key: name, message: data, appGuid: this._appGuid });
    rawPostMessage(this, msg, name === 'iframe.handshake');
  },

  /// #### client.on(name, handler, [context])
  ///
  /// Allows you to add handlers to a framework event. You can add as many handler as you wish.
  /// They will be executed in the order they were added.
  ///
  /// ##### Arguments
  ///
  ///   * `name` the name of the framework event you want to listen to. This can be
  ///     [framework](./events.html#framework-events), [request](./events.html#request-events), or
  ///     [custom](./events.html#custom-events) events. Your iframe can listen to any events your app
  ///     receives, apart from DOM events. You don't need to register these events in the app first
  ///   * `handler` a function to be called when this event fires. You can expect to receive the same
  ///     event object your app would receive, parsed as JSON
  ///   * `context` (optional) the value of `this` within your handler
  ///
  /// ```javascript
  /// var client = ZAFClient.init();
  /// client.on('app.registered', function(e) {
  ///   // go nuts
  /// });
  /// ```
  ///
  /// Note: As soon as communication with the Zendesk app is established, the SDK triggers an
  /// `app.registered` event. You can add as many handlers to `app.registered` as you like. They're
  /// called immediately after the `init` callback.
  on: function(name, handler, context) {
    if (typeof handler == 'function') {
      handler = context ?
        handler.bind(context) :
        handler;

      this._messageHandlers[name] = this._messageHandlers[name] || [];
      this._messageHandlers[name].push(handler);

      if (name !== 'app.registered') {
        this.postMessage('iframe.on:' + name);
      }
    }
  },

  /// #### client.off(name, handler)
  ///
  /// Allows you to remove a handler for a framework event.
  ///
  /// ##### Arguments
  ///
  ///   * `name` the name of the event
  ///   * `handler` the function you attached earlier with `on`
  ///
  /// ```javascript
  /// var client = ZAFClient.init();
  ///
  /// client.on('app.registered', function appRegistered(e) {
  ///   // do stuff then remove the handler
  ///   client.off('app.registered', appRegistered);
  /// });
  /// ```
  off: function(name, handler) {
    if (!this._messageHandlers[name]) { return false; }
    var index = this._messageHandlers[name].indexOf(handler);
    this.postMessage('iframe.off:' + name);
    return this._messageHandlers[name].splice(index, 1)[0];
  },

  /// #### client.has(name, handler)
  ///
  /// Returns whether or not an event has the specified handler attached to it.
  ///
  /// ##### Arguments
  ///
  ///   * `name` the name of the event
  ///   * `handler` the handler you want to test
  ///
  /// ```javascript
  /// var client = ZAFClient.init();
  ///
  /// client.on('app.registered', function appRegistered(e) {
  ///   // do stuff
  /// });
  ///
  /// client.has('app.registered', appRegistered); // true
  /// client.has('app.activated', appRegistered); // false
  /// ```
  has: function(name, handler) {
    if (!this._messageHandlers[name]) { return false; }
    return this._messageHandlers[name].indexOf(handler) !== -1;
  },

  /// #### client.trigger(name, [data])
  ///
  /// Triggers the specified event on the client.
  ///
  /// ##### Arguments
  ///
  ///   * `name` the name of the event you want to trigger
  ///   * `data` (optional) data you want to pass to the handler
  ///
  /// ```javascript
  /// var client = ZAFClient.init();
  ///
  /// client.on('activation', function {
  ///   console.log('activating!')
  /// });
  ///
  /// client.trigger('activation') // activating!
  /// ```
  trigger: function(name, data) {
    if (!this._messageHandlers[name]) { return false; }
    this._messageHandlers[name].forEach(function(handler) {
      handler(data);
    });
  },

  /// #### client.request(options)
  ///
  /// Dispatch [requests](./requests) via the Zendesk app.
  ///
  /// ##### Arguments
  ///
  ///   * `options` the url of the request or an options object containing a url key/value
  ///
  /// ##### Returns
  ///
  /// A [Promises/A+](https://promisesaplus.com) conformant `promise` object.
  ///
  /// ```javascript
  /// var client = ZAFClient.init();
  ///
  /// client.request('/api/v2/tickets.json').then(
  ///   function(tickets) {
  ///     console.log(tickets);
  ///   },
  ///   function(response) {
  ///     console.error(response.responseText);
  ///   }
  /// );
  /// ```
  request: function(options) {
    var requestKey = 'request:' + nextIdFor('request');

    return new Promise(function(resolve, reject) {
      if (typeof options === 'string') {
        options = { url: options };
      }

      this.on(requestKey + '.done', function(evt) {
        resolve.apply(this, evt.responseArgs);
      });

      this.on(requestKey + '.fail', function(evt) {
        reject.apply(this, evt.responseArgs);
      });

      this.postMessage(requestKey, options);
    }.bind(this));
  },

  metadata: function() {
    return new Promise(function(resolve) {
      if (this._metadata) {
        resolve(this._metadata);
      } else {
        this.on('app.registered', function() {
          resolve(this._metadata);
        }.bind(this));
      }
    }.bind(this));
  },

  context: function() {
    return new Promise(function(resolve) {
      if (this._context) {
        resolve(this._context);
      } else {
        this.on('app.registered', function() {
          resolve(this._context);
        }.bind(this));
      }
    }.bind(this));
  },

  // Accepts string or array of strings.
  get: function(path) {
    var paths = Array.isArray(path) ? path : [path];

    if (paths.some(function(s) {return typeof s !== 'string'; })) {
      throw new Error('The get method accepts a string or array of strings.');
    }

    return wrappedPostMessage.call(this, 'get', paths);
  },

  set: function(key, val) {
    var obj = key;

    if (typeof key === 'string') {
      if (arguments.length === 1) {
        throw new Error('The setter requires a value');
      }
      obj = {};
      obj[key] = val;
    }

    if (typeof obj !== 'object' || Array.isArray(obj)) {
      throw new Error('The set method accepts 2 string, or an object.');
    }

    return wrappedPostMessage.call(this, 'set', obj);
  },

  invoke: function(key) {
    var obj = key;

    if (typeof key === 'string') {
      obj = {};
      obj[key] = Array.prototype.slice.call(arguments, 1);
    } else {
      throw new Error('Invoke with an object isn\'t supported.');
    }

    return wrappedPostMessage.call(this, 'invoke', obj);
  }
};

module.exports = Client;
<|endoftext|>