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)
}