UNPKG

prisme-flow

Version:

prisme platform flow engine

458 lines (382 loc) 16.6 kB
/** * Created by prisme.io on 09/06/2017. */ module.exports = function (RED) { 'use strict' // needed for auth var crypto = require('crypto') var Url = require('url') var request = require('request') var IMAGE = 'image'// currently we're only considering images var repeat = 900000 // 15 minutes => the repeat frequency of the input node function InstagramCredentialsNode (n) { RED.nodes.createNode(this, n) } function downloadImageAndSendAsBuffer (node, url, msg) { request({ uri: url, encoding: null}, function (error, response, body) { if (!error && response.statusCode === 200) { msg.payload = body node.send(msg) } else { node.warn(RED._('Instagram node has failed to buffer up an image. Image was not sent. __error__ __response__ ', {error: error, response: response})) } }) } // the query node doesn't require special initialization as it serves the latest photo anyway function initializeQueryNode (node) { if (node.instagramConfig && node.instagramConfig.credentials) { if (!node.instagramConfig.credentials.access_token) { node.warn(RED._('Missing Instagram access token. Authorization has not been completed before node initialization.')) return } } else { node.warn(RED._('Missing Instagram configuration or credentials. Authorization has not been completed before node initialization.')) return } node.ig.use({ access_token: node.instagramConfig.credentials.access_token}) node.on('input', function (msg) { handleQueryNodeInput(node, msg) }) } // we initialize the node: load access token, obtain current state from Instagram function initializeInputNode (node) { if (node.instagramConfig && node.instagramConfig.credentials) { if (!node.instagramConfig.credentials.access_token) { node.warn(RED._('Missing Instagram access token. Authorization has not been completed before node initialization.')) return } } else { node.warn(RED._('Missing Instagram configuration or credentials. Authorization has not been completed before node initialization.')) return } node.ig.use({ access_token: node.instagramConfig.credentials.access_token}) // Now grab initial state but only grab the ones we're concerned with if (node.inputType === 'photo') { node.ig.user_media_recent('self', { count: 1, min_id: null, max_id: null}, function (err, medias, pagination, remaining, limit) { if (err) { node.warn(RED._('Instagram node has failed to fetch latest user photo : __err__', {err: err})) } if (medias.length > 0) { // if the user has uploaded something to Instagram already node.latestSelfContentID = medias[0].id } node.on('input', function (msg) { msg = {} handleInputNodeInput(node, msg) }) node.interval = setInterval(function () { // self trigger node.emit('input', {}) }, repeat) }) } else if (node.inputType === 'like') { node.ig.user_self_liked({ count: 1, max_like_id: null}, function (err, medias, pagination, remaining, limit) { if (err) { node.warn(RED._('Instagram node has failed to fetch latest liked photo : __err__', {err: err})) } if (medias.length > 0) { // if the user has liked something to Instagram already node.latestLikedID = medias[0].id } node.on('input', function (msg) { msg = {} handleInputNodeInput(node, msg) }) node.interval = setInterval(function () { // self trigger node.emit('input', {}) }, repeat) }) } } function handleQueryNodeInput (node, msg) { if (node.inputType === 'photo') { node.ig.user_media_recent('self', { count: 1, min_id: null, max_id: null}, function (err, medias, pagination, remaining, limit) { if (err) { node.warn(RED._('Instagram node has failed to fetch latest liked photo : __err__', {err: err})) } if (medias.length > 0) { // if the user has uploaded something to Instagram already if (medias[0].type === IMAGE) { if (medias[0].location) { if (medias[0].location.latitude) { if (!msg.location) { msg.location = {} } msg.location.lat = medias[0].location.latitude } if (medias[0].location.longitude) { if (!msg.location) { msg.location = {} } msg.location.lon = medias[0].location.longitude } } if (medias[0].created_time) { msg.time = new Date(medias[0].created_time * 1000) } var url if (medias[0].images && medias[0].images.standard_resolution && medias[0].images.standard_resolution.url) { url = medias[0].images.standard_resolution.url } else { node.warn(RED._('The Instagram API has not provided all the required data to establish the URL of the photo. Ignoring media.')) return } if (node.outputType === 'link') { msg.payload = url node.send(msg) } else if (node.outputType === 'buffer') { downloadImageAndSendAsBuffer(node, url, msg) } } else { node.warn(RED._('The most recent media on Instagram is not a photo, therefore it has been ignored.')) } } else { msg.payload = null node.send(msg) node.warn(RED._('The user has not uploaded any media to Instagram yet, null payload has been sent')) } }) } else if (node.inputType === 'like') { node.ig.user_self_liked({ count: 1, max_like_id: null}, function (err, medias, pagination, remaining, limit) { if (err) { node.warn(RED._('Instagram node has failed to fetch latest liked photo : __err__', {err: err})) } if (medias.length > 0) { // if the user has liked something to Instagram already if (medias[0].type === IMAGE) { if (medias[0].location) { if (medias[0].location.latitude) { if (!msg.location) { msg.location = {} } msg.location.lat = medias[0].location.latitude } if (medias[0].location.longitude) { if (!msg.location) { msg.location = {} } msg.location.lon = medias[0].location.longitude } } if (medias[0].created_time) { msg.time = new Date(medias[0].created_time * 1000) } var url if (medias[0].images && medias[0].images.standard_resolution && medias[0].images.standard_resolution.url) { url = medias[0].images.standard_resolution.url } else { node.warn(RED._('The Instagram API has not provided all the required data to establish the URL of the photo. Ignoring media.')) return } if (node.outputType === 'link') { msg.payload = url node.send(msg) } else if (node.outputType === 'buffer') { downloadImageAndSendAsBuffer(node, url, msg) } } else { node.warn(RED._('The most recently liked media on Instagram is not a photo, therefore it has been ignored.')) } } else { msg.payload = null node.send(msg) node.warn(RED._('The user has not liked any media on Instagram yet, null payload has been sent')) } }) } } function handleInputNodeInput (node, msg) { var areWeInPaginationRecursion = false var idOfLikedReturned var idOfSelfReturned var returnPagefulsOfStuff = function (err, medias, pagination, remaining, limit) { var carryOnPaginating = true if (err) { node.warn(RED._('Instagram node has failed to fetch latest media : __err__', {err: err})) } if (medias) { for (var i = 0; i < medias.length; i++) { if (node.inputType === 'like') { // like is a special case as per Instagram API behaviour if (areWeInPaginationRecursion === false) { // need to set the pointer of latest served liked image before pagination occurs idOfLikedReturned = medias[0].id } if (medias[i].id === node.latestLikedID || node.latestLikedID === null) { // we finally found the image we already returned or has been there at init node.latestLikedID = idOfLikedReturned // we need to assign the latest liked to the one we returned first => can only do node at the end, otherwise we'd never match break condition and always return everything carryOnPaginating = false break } } if (node.inputType === 'photo' && i === 0 && (areWeInPaginationRecursion === false)) { // only set the served self content ID to equal the first media of the first pagination page and ignore on subsequent pages idOfSelfReturned = medias[i].id } if (node.inputType === 'photo' && (medias[i].id === node.latestSelfContentID)) { // if we say to the Insta API that we want images more recent than image id "blah", it returns image with that id too // deliberate no-op } else if (medias[i].type === IMAGE) { var url = medias[i].images.standard_resolution.url if (medias[i].location) { if (medias[i].location.latitude) { if (!msg.location) { msg.location = {} } msg.location.lat = medias[i].location.latitude } if (medias[i].location.longitude) { if (!msg.location) { msg.location = {} } msg.location.lon = medias[i].location.longitude } } if (medias[i].created_time) { msg.time = new Date(medias[i].created_time * 1000) } if (node.outputType === 'link') { msg.payload = url node.send(msg) } else if (node.outputType === 'buffer') { downloadImageAndSendAsBuffer(node, url, msg) } } } } else if (areWeInPaginationRecursion === false) { node.warn(RED._('Instagram node has failed to fetch any media')) return } if (pagination && pagination.next && carryOnPaginating) { areWeInPaginationRecursion = true pagination.next(returnPagefulsOfStuff) } else { node.latestSelfContentID = idOfSelfReturned } } // If we're processing user content if (node.inputType === 'photo') { node.ig.user_media_recent('self', { count: null, min_id: node.latestSelfContentID, max_id: null}, returnPagefulsOfStuff) } else if (node.inputType === 'like') { // If we're processing likes node.ig.user_self_liked({ count: null, max_like_id: null}, returnPagefulsOfStuff) } } function InstagramInNode (n) { RED.nodes.createNode(this, n) var node = this node.ig = require('instagram-node').instagram() node.latestSelfContentID = null // if the user has not liked/uploaded any content yet node.latestLikedID = null node.inputType = n.inputType node.outputType = n.outputType node.interval = null // used to track individual refresh intervals node.instagramConfig = RED.nodes.getNode(n.instagram) if (!node.instagramConfig) { node.warn(RED._('iMissing Instagram credentials')) return } initializeInputNode(node) // the build in poll interval is getting set up at the end of init node.on('close', function () { if (node.interval !== null) { clearInterval(node.interval) } node.latestSelfContentID = null node.latestLikedID = null node.inputType = null node.outputType = null }) } function InstagramNode (n) { RED.nodes.createNode(this, n) var node = this node.ig = require('instagram-node').instagram() node.inputType = n.inputType node.outputType = n.outputType node.instagramConfig = RED.nodes.getNode(n.instagram) if (!node.instagramConfig) { node.warn(RED._('Missing Instagram credentials')) return } initializeQueryNode(node) node.on('close', function () { node.inputType = null node.outputType = null }) } RED.nodes.registerType('instagram-credentials', InstagramCredentialsNode, { credentials: { username: {type: 'text'}, clientID: {type: 'text'}, redirectURI: { type: 'text'}, access_token: {type: 'password'} } }) RED.nodes.registerType('instagram', InstagramNode) RED.nodes.registerType('instagram in', InstagramInNode) RED.httpAdmin.get('/instagram-credentials/auth', function (req, res) { var node_id = req.query.node_id var credentials = RED.nodes.getCredentials(node_id) || {} credentials.client_id = req.query.client_id credentials.client_secret = req.query.client_secret credentials.redirect_uri = req.query.redirect_uri if (!credentials.client_id || !credentials.client_secret || !credentials.redirect_uri) { return res.send(RED._('The callback from Instagram did not contain a required code')) } var csrfToken = crypto.randomBytes(18).toString('base64').replace(/\//g, '-').replace(/\+/g, '_') credentials.csrfToken = csrfToken res.redirect(Url.format({ protocol: 'https', hostname: 'api.instagram.com', pathname: '/oauth/authorize/', query: { client_id: credentials.client_id, redirect_uri: credentials.redirect_uri, response_type: 'code', state: node_id + ':' + credentials.csrfToken } })) RED.nodes.addCredentials(node_id, credentials) }) RED.httpAdmin.get('/instagram-credentials/auth/callback', function (req, res) { var state = req.query.state.split(':') var node_id = state[0] var csrfToken = state[1] var credentials = RED.nodes.getCredentials(node_id) || {} if (!credentials || !credentials.client_id || !credentials.client_secret || !credentials.redirect_uri) { return res.send(RED._('ERROR: Received query from UI without the needed credentials')) } if (csrfToken !== credentials.csrfToken) { return res.status(401).send(RED._('CSRF token mismatch, possible cross-site request forgery attempt.')) } RED.nodes.deleteCredentials(node_id) // we don't want to keep the csrfToken // from now on, credentials are in memory only delete credentials.csrfToken if (!req.query.code) { return res.status(400).send(RED._('The callback from Instagram did not contain a required code')) } credentials.code = req.query.code request.post({ url: 'https://api.instagram.com/oauth/access_token', json: true, form: { client_id: credentials.client_id, client_secret: credentials.client_secret, grant_type: 'authorization_code', redirect_uri: credentials.redirect_uri, code: credentials.code } }, function (err, result, data) { if (err) { return res.send(RED._('request error:__err__', {err: err})) } if (data.error) { return res.send(RED._('oauth error: __error__', {error: data.error})) } if (result.statusCode !== 200) { return res.send(RED._('Instagram replied with the unexpected HTTP status code of __statusCode__ Details: __data__', {statusCode: result.statusCode, data: data})) } if (data.user.username) { credentials.username = data.user.username } else { return res.send(RED._('Error! Instagram node has failed to fetch the username.')) } if (data.access_token) { credentials.access_token = data.access_token } else { return res.send(RED._('Error! Instagram node has failed to fetch a valid access token.')) } RED.nodes.addCredentials(node_id, credentials) res.send(RED._('Successfully authorized with Instagram. You can close this window now.')) }) }) }