Source: routes/stories.js

module.exports = Stories

var qs = require('querystring')

var debug = require('debug')('snapchat:stories')
var async = require('async')
var Promise = require('bluebird')

var constants = require('../lib/constants')
var Request = require('../lib/request')
var StringUtils = require('../lib/string-utils')

var SKBlob = require('../models/blob')
var StoryUpdater = require('../models/story-updater')
var SharedStoryDescription = require('../models/shared-story-description')

/**
 * Snapchat wrapper for story-related API calls.
 *
 * @class
 * @param {Object} opts
 */
function Stories (client, opts) {
  var self = this
  if (!(self instanceof Stories)) return new Stories(client, opts)
  if (!opts) opts = {}

  self.client = client
}

/**
 * Posts a story with the given options.
 *
 * @param {Blob} blob The Blob object containing the image or video data to send.
 * @param {StoryOptions} opts The options for the story to post.
 * @param {function} cb
 */
Stories.prototype.postStory = function (blob, opts, cb) {
  var self = this

  return new Promise(function (resolve, reject) {
    debug('Stories.postStory')

    self._uploadStory(blob, function (err, mediaID) {
      if (err) {
        debug('Snapchat.Stories.postStory error %s', err)
        return reject(err)
      }

      self.client.post(constants.endpoints.stories.post, {
        'caption_text_display': opts.text,
        'story_timestamp': StringUtils.timestamp(),
        'type': blob.isImage ? constants.MediaKind.Image : constants.MediaKind.Video,
        'media_id': mediaID,
        'client_id': mediaID,
        'time': opts.timer | 0,
        'username': self.client.username,
        'camera_front_facing': opts.cameraFrontFacing,
        'my_story': 'true',
        'zipped': 0,
        'shared_ids': '{}'
      }, function (err, result) {
        if (err) {
          debug('Snapchat.Stories.postStory error %s', err)
          return reject(err)
        } else if (result) {
          return resolve(result)
        }

        return reject(new Error('Snapchat.Stories.postStory parse error'))
      })
    })
  }).nodeify(cb)
}

/**
 * Downloads media for a story.
 *
 * @param {Story} story The story to download.
 * @param {function} cb SKBlob
 */
Stories.prototype.loadStoryBlob = function (story, cb) {
  var self = this

  return new Promise(function (resolve, reject) {
    debug('Stories.loadStoryBlob (%s)', story.identifier)

    function blobHandler (err, body) {
      if (err) {
        debug('Snapchat.Stories.loadStoryBlob error %s', err)
        return reject(err)
      }

      SKBlob.initWithStoryData(body, story, function (err, blob) {
        if (err) {
          return reject(err)
        }
        return resolve(blob)
      })
    }

    if (story.needsAuth) {
      var url = constants.endpoints.stories.authBlob + story.mediaIdentifier

      Request.post(url, {
        'story_id': story.mediaIdentifier,
        'username': self.client.username
      }, self.client.googleAuthToken, self.client.authToken, blobHandler)
    } else {
      var baseIgnorePattern = new RegExp(constants.core.baseList.join('|'))
      self.client.get(story.mediaURL.replace(baseIgnorePattern, ''), blobHandler)
    }
  }).nodeify(cb)
}

/**
 * Downloads the thumbnail for a story.
 *
 * @param story The story whose thumbnail you wish to download.
 * @param {function} cb SKBlob
 */
Stories.prototype.loadStoryThumbnailBlob = function (story, cb) {
  var self = this

  return new Promise(function (resolve, reject) {
    debug('Stories.loadStoryThumbnailBlob (%s)', story.identifier)

    self.client.get(constants.endpoints.stories.thumb + story.mediaIdentifier, function (err, body) {
      if (err) {
        debug('Snapchat.Stories.loadStoryThumbnailBlob error %s', err)
        return reject(err)
      }

      SKBlob.initWithStoryData(body, story, function (err, blob) {
        if (err) {
          return reject(err)
        }
        return resolve(blob)
      })
    })
  }).nodeify(cb)
}

/**
 * Batch loads media for a set of stories.
 *
 * @param {Array<Story>} stories An array of Story objects whose media you wish to download.
 * @param {function} cb
 */
Stories.prototype.loadStories = function (stories, cb) {
  debug('Stories.loadStories (%d)', stories.length)

  return new Promise(function (resolve, reject) {
    var results = {
      loaded: [ ],
      failed: [ ],
      errors: [ ]
    }

    async.eachLimit(stories, 4, function (story, cb) {
      story.load(function (err) {
        if (err) {
          results.failed.push(story)
          results.errors.push(err)
        } else {
          results.loaded.push(story)
        }

        return cb(err)
      })
    }, function (err) {
      if (err) {
        return reject(err)
      }
      return resolve(results)
    })
  }).nodeify(cb)
}

/**
 * Deletes a story of yours.
 *
 * @param {UserStory} story
 * @param {function} cb
 */
Stories.prototype.deleteStory = function (story, cb) {
  var self = this

  return new Promise(function (resolve, reject) {
    debug('Stories.deleteStory (%s)', story.identifier)

    self.client.post(constants.endpoints.stories.remove, {
      'story_id': story.identifier,
      'username': self.client.username
    }, function (err) {
      if (err) {
        return reject(err)
      }
      var index = self.client.session.userStories.indexOf(story)
      if (index >= 0) {
        self.client.session.userStories.splice(index, 1)
      }
      return resolve()
    })
  }).nodeify(cb)
}

/**
 * Marks a set of stories as opened.
 *
 * @param {Array<StoryUpdater>} stories An array of StoryUpdater objects.
 * @param {function} cb
 */
Stories.prototype.markStoriesViewed = function (stories, cb) {
  var self = this
  debug('Stories.markStoriesViewed (%d)', stories.length)

  var friendStories = stories.map(function (update) {
    return {
      'id': update.storyID,
      'screenshot_count': update.screenshotCount,
      'timestamp': update.timestamp
    }
  })

  return self.client.post(constants.endpoints.update.stories, {
    'username': self.client.username,
    'friend_stories': JSON.stringify(friendStories)
  }, cb)
}

/**
 * Marks a single story opened.
 * To batch mark stories viewed, use markStoriesViewed
 *
 * @param {Story} story The story to mark as opened.
 * @param {number} sscount The number of times the story was screenshotted.
 * @param {function} cb
 */
Stories.prototype.markStoryViewed = function (story, sscount, cb) {
  var self = this
  debug('Stories.markStoryViewed (%s)', story.identifier)

  return self.markStoriesViewed([
    new StoryUpdater(story.identifier, StringUtils.timestamp(), sscount)
  ], cb)
}

/**
 * Hides a shared story from the story feed.
 *
 * @param {function} cb
 */
Stories.prototype.hideSharedStory = function (story, cb) {
  var self = this
  debug('Stories.hideSharedStory (%s)', story.identifier)

  return self.client.post(constants.endpoints.friends.hide, {
    'friend': story.username,
    'hide': 'true',
    'username': self.client.username
  }, cb)
}

/**
 * Does nothing if the story is not a shared story.
 *
 * @param {Story} sharedStory A shared story.
 * @param {function} cb
 */
Stories.prototype.provideSharedDescription = function (sharedStory, cb) {
  var self = this
  debug('Stories.provideSharedDescription (%s)', sharedStory.identifier)
  if (!sharedStory.shared) {
    return Promise.reject(new Error('Snapchat.Stories.provideSharedDescription error')).nodeify(cb)
  }

  return self.client.post(constants.endpoints.sharedDescription, {
    'shared_id': sharedStory.identifier,
    'username': self.client.username
  }, cb)
}

/**
 * Retrieves the description for a shared story.
 *
 * @param sharedStory A shared story.
 * @param {function} cb
 */
Stories.prototype.getSharedDescriptionForStory = function (sharedStory, cb) {
  var self = this

  return new Promise(function (resolve, reject) {
    debug('Stories.getSharedDescriptionForStory (%s)', sharedStory.identifier)

    if (!sharedStory.sharedStoryIdentifier) {
      return reject(new Error('Snapchat.Stories.getSharedDescriptionForStory error invalid story'))
    }

    var endpoint = constants.endpoints.sharedDescription + '?' + qs.stringify({
      'ln': 'en',
      'shared_id': sharedStory.sharedStoryIdentifier
    })

    self.client.get(endpoint, function (err, result) {
      if (err) {
        return reject(err)
      } else if (result) {
        return resolve(new SharedStoryDescription(result))
      }

      return reject(new Error('Snapchat.Stories.getSharedDescriptionForStory parse error'))
    })
  }).nodeify(cb)
}

/**
 * Uploads a new story associated with the given blob.
 *
 * @private
 * @param {SKBlob} blob
 * @param {function} cb
 */
Stories.prototype._uploadStory = function (blob, cb) {
  var self = this
  var uuid = StringUtils.mediaIdentifer(self.client.username)

  var params = {
    'media_id': uuid,
    'type': blob.isImage ? constants.MediaKind.Image : constants.MediaKind.Video,
    'data': blob.data,
    'zipped': 0,
    'features_map': '{}',
    'username': self.client.username
  }

  var headers = { }

  headers[constants.headers.clientAuthToken] = 'Bearer ' + self.client.googleAuthToken
  headers[constants.headers.contentType] = 'multipart/form-data; boundary=' + constants.core.boundary

  return Request.postCustom(constants.endpoints.stories.upload, params, headers, self.client.authToken, cb)
}