Source: client.js

var request = require('request');
var StreamFeed = require('./feed');
var signing = require('./signing');
var errors = require('./errors');
var utils = require('./utils');
var BatchOperations = require('./batch_operations');

var StreamClient = function() {
  /**
   * Client to connect to Stream api
   * @class StreamClient
   */ 
  this.initialize.apply(this, arguments);
};

StreamClient.prototype = {
  baseUrl: 'https://api.getstream.io/api/',

  initialize: function(apiKey, apiSecret, appId, options) {
    /**
     * Initialize a client
     * @method intialize
     * @memberof StreamClient.prototype
     * @param {string} apiKey - the api key
     * @param {string} [apiSecret] - the api secret 
     * @param {string} [appId] - id of the app
     * @param {object} options - additional options
     * @param {string} options.location - which data center to use
     * @example <caption>initialize is not directly called by via stream.connect, ie:</caption>
     * stream.connect(apiKey, apiSecret)
     * @example <caption>secret is optional and only used in server side mode</caption>
     * stream.connect(apiKey, null, appId);
     */
    this.apiKey = apiKey;
    this.apiSecret = apiSecret;
    this.appId = appId;
    this.options = options || {};
    this.version = this.options.version || 'v1.0';
    this.fayeUrl = this.options.fayeUrl || 'https://faye.getstream.io/faye';
    this.fayeClient = null;
    // track a source name for the api calls, ie get started or databrowser
    this.group = this.options.group || 'unspecified';
    // track subscriptions made on feeds created by this client
    this.subscriptions = {};
    // which data center to use
    this.location = this.options.location;
    if (this.location) {
      this.baseUrl = 'https://' + this.location + '-api.getstream.io/api/';
    }

    if (typeof (process) !== 'undefined' && process.env.LOCAL) {
      this.baseUrl = 'http://localhost:8000/api/';
    }

    if (typeof (process) !== 'undefined' && process.env.LOCAL_FAYE) {
      this.fayeUrl = 'http://localhost:9999/faye/';
    }

    this.handlers = {};
    this.browser = typeof (window) !== 'undefined';
    this.node = !this.browser;

    if (this.browser && this.apiSecret) {
      throw new errors.FeedError('You are publicly sharing your private key. Dont use the private key while in the browser.');
    }
  },

  on: function(event, callback) {
    /**
     * Support for global event callbacks
     * This is useful for generic error and loading handling
     * @method on
     * @memberof StreamClient.prototype
     * @param {string} event - Name of the event
     * @param {function} callback - Function that is called when the event fires
     * @example
     * client.on('request', callback);
     * client.on('response', callback);
     */
    this.handlers[event] = callback;
  },

  off: function(key) {
    /**
     * Remove one or more event handlers
     * @method off
     * @memberof StreamClient.prototype
     * @param {string} [key] - Name of the handler
     * @example
     * client.off() removes all handlers
     * client.off(name) removes the specified handler
     */
    if (key === undefined) {
      this.handlers = {};
    } else {
      delete this.handlers[key];
    }
  },

  send: function() {
    /**
     * Call the given handler with the arguments
     * @method send
     * @memberof StreamClient.prototype
     * @access private
     */
    var args = Array.prototype.slice.call(arguments);
    var key = args[0];
    args = args.slice(1);
    if (this.handlers[key]) {
      this.handlers[key].apply(this, args);
    }
  },

  wrapCallback: function(cb) {
    /**
     * Wrap callback for HTTP request
     * @method wrapCallBack
     * @memberof StreamClient.prototype
     * @access private
     */
    var client = this;

    function callback() {
      // first hit the global callback, subsequently forward
      var args = Array.prototype.slice.call(arguments);
      var sendArgs = ['response'].concat(args);
      client.send.apply(client, sendArgs);
      if (cb !== undefined) {
        cb.apply(client, args);
      }
    }

    return callback;
  },

  userAgent: function() {
    var description = (this.node) ? 'node' : 'browser';
    // TODO: get the version here in a way which works in both and browserify
    var version = 'unknown';
    return 'stream-javascript-client-' + description + '-' + version;
  },

  getReadOnlyToken: function(feedSlug, userId) {
    /**
     * Returns a token that allows only read operations
     *
     * @method getReadOnlyToken 
     * @memberOf StreamClient.prototype
     * @param {string} feedSlug - The feed slug to get a read only token for
     * @param {string} userId - The user identifier
     * @return {string} token
     * @example
     * client.getReadOnlyToken('user', '1');
     */
    var feedId = '' + feedSlug + userId;
    return signing.JWTScopeToken(this.apiSecret, feedId, '*', 'read');
  },

  getReadWriteToken: function(feedSlug, userId) {
    /**
     * Returns a token that allows read and write operations
     *
     * @method getReadWriteToken
     * @memberOf StreamClient.prototype
     * @param {string} feedSlug - The feed slug to get a read only token for
     * @param {string} userId - The user identifier
     * @return {string} token
     * @example
     * client.getReadWriteToken('user', '1');
     */
    var feedId = '' + feedSlug + userId;
    return signing.JWTScopeToken(this.apiSecret, feedId, '*', '*');
  },

  feed: function(feedSlug, userId, token, siteId, options) {
    /**
     * Returns a feed object for the given feed id and token
     * @method feed
     * @memberOf StreamClient.prototype
     * @param {string} feedSlug - The feed slug
     * @param {string} userId - The user identifier
     * @param {string} [token] - The token
     * @param {string} [siteId] - The site identifier
     * @param {object} [options] - Additional function options 
     * @param {boolean} [options.readOnly] - A boolean indicating whether to generate a read only token for this feed
     * @return {Feed}
     * @example
     * client.feed('user', '1', 'token2');
     */

    options = options || {};

    if (!feedSlug || !userId) {
      throw new errors.FeedError('Please provide a feed slug and user id, ie client.feed("user", "1")');
    }

    if (feedSlug.indexOf(':') !== -1) {
      throw new errors.FeedError('Please initialize the feed using client.feed("user", "1") not client.feed("user:1")');
    }

    utils.validateFeedSlug(feedSlug);
    utils.validateUserId(userId);

    // raise an error if there is no token
    if (!this.apiSecret && !token) {
      throw new errors.FeedError('Missing token, in client side mode please provide a feed secret');
    }

    // create the token in server side mode
    if (this.apiSecret && !token) {
      var feedId = '' + feedSlug + userId;
      // use scoped token if read-only access is necessary
      token = options.readOnly ? this.getReadOnlyToken(feedSlug, userId) : signing.sign(this.apiSecret, feedId);
    }

    var feed = new StreamFeed(this, feedSlug, userId, token, siteId);
    return feed;
  },

  enrichUrl: function(relativeUrl) {
    /**
     * Combines the base url with version and the relative url
     * @access private
     */
    var url = this.baseUrl + this.version + '/' + relativeUrl;
    return url;
  },

  enrichKwargs: function(kwargs) {
    /**
     * Adds the API key and the signature
     * @access private
     */
    kwargs.url = this.enrichUrl(kwargs.url);
    if (kwargs.qs === undefined) {
      kwargs.qs = {};
    }

    kwargs.qs['api_key'] = this.apiKey;
    kwargs.qs.location = this.group;
    kwargs.json = true;
    var signature = kwargs.signature || this.signature;
    kwargs.headers = {};

    // auto-detect authentication type and set HTTP headers accordingly
    if (signing.isJWTSignature(signature)) {
      kwargs.headers['stream-auth-type'] = 'jwt';
      signature = signature.split(' ').reverse()[0];
    } else {
      kwargs.headers['stream-auth-type'] = 'simple';
    }

    kwargs.headers.Authorization = signature;
    kwargs.headers['X-Stream-Client'] = this.userAgent();
    return kwargs;
  },

  signActivity: function(activity) {
    return this.signActivities([activity])[0];
  },

  signActivities: function(activities) {
    /**
     * We automatically sign the to parameter when in server side mode
     * @access private
     */
    if (!this.apiSecret) {
      return activities;
    }

    for (var i = 0; i < activities.length; i++) {
      var activity = activities[i];
      var to = activity.to || [];
      var signedTo = [];
      for (var j = 0; j < to.length; j++) {
        var feedId = to[j];
        var feedSlug = feedId.split(':')[0];
        var userId = feedId.split(':')[1];
        var token = this.feed(feedSlug, userId).token;
        var signedFeed = feedId + ' ' + token;
        signedTo.push(signedFeed);
      }

      activity.to = signedTo;
    }

    return activities;
  },

  getFayeAuthorization: function() {
      var apiKey = this.apiKey,
          self = this;

      return {
        incoming: function(message, callback) {
          callback(message);
        },

        outgoing: function(message, callback) {
          if (message.subscription && self.subscriptions[message.subscription]) {
            var subscription = self.subscriptions[message.subscription];

            message.ext = {
              'user_id': subscription.userId,
              'api_key': apiKey,
              'signature': subscription.token,
            };
          }

          callback(message);
        },
      };
    },

  getFayeClient: function() {
      var Faye = require('faye');
      if (this.fayeClient === null) {
        this.fayeClient = new Faye.Client(this.fayeUrl);
        var authExtension = this.getFayeAuthorization();
        this.fayeClient.addExtension(authExtension);
      }

      return this.fayeClient;
    },

  /*
   * Shortcuts for post, get and delete HTTP methods
   *
   */

  get: function(kwargs, cb) {
    this.send('request', 'get', kwargs, cb);
    kwargs = this.enrichKwargs(kwargs);
    kwargs.method = 'GET';
    var callback = this.wrapCallback(cb);
    return request(kwargs, callback);
  },

  post: function(kwargs, cb) {
    this.send('request', 'post', kwargs, cb);
    kwargs = this.enrichKwargs(kwargs);
    kwargs.method = 'POST';
    var callback = this.wrapCallback(cb);
    return request(kwargs, callback);
  },

  delete: function(kwargs, cb) {
    this.send('request', 'delete', kwargs, cb);
    kwargs = this.enrichKwargs(kwargs);
    kwargs.method = 'DELETE';
    var callback = this.wrapCallback(cb);
    return request(kwargs, callback);
  },

};

// If we are in a node environment and batchOperations is available add the methods to the prototype of StreamClient
if (BatchOperations) {
  for (var key in BatchOperations) {
    if (BatchOperations.hasOwnProperty(key)) {
      StreamClient.prototype[key] = BatchOperations[key];
    }
  }
}

module.exports = StreamClient;