UNPKG

prisme-flow

Version:

prisme platform flow engine

573 lines (548 loc) 18.3 kB
/** * Created by prisme.io on 09/06/2017. */ module.exports = function (RED) { 'use strict' function GoogleCalendarInputNode (n) { RED.nodes.createNode(this, n) this.google = RED.nodes.getNode(n.google) if (!this.google || !this.google.credentials.accessToken) { this.warn(RED._('Missing google credentials')) return } this.calendar = n.calendar || 'primary' if (!n.offsetType || n.offsetType === 'at') { this.offset = 0 } else { var plusOrMinus = n.offsetType === 'before' ? 1 : -1 var multiplier = { seconds: 1000, minutes: 60 * 1000, hours: 60 * 60 * 1000, days: 24 * 60 * 60 * 1000 }[n.offsetUnits] this.offset = plusOrMinus * n.offset * multiplier } var setNextTimeout var eventsBetween if (!n.offsetFrom || n.offsetFrom === 'start') { setNextTimeout = setNextStartingTimeout eventsBetween = eventsStartingBetween } else { setNextTimeout = setNextEndingTimeout eventsBetween = eventsEndingBetween } var node = this node.status({fill: 'blue', shape: 'dot', text: 'querying'}) calendarList(node, function (err) { if (err) { node.error(err, {}) node.status({fill: 'red', shape: 'ring', text: 'failed'}) return } var cal = calendarByNameOrId(node, node.calendar) if (!cal) { node.status({fill: 'red', shape: 'ring', text: 'invalid calendar'}) return } node.status({}) node.on('input', function (msg) { node.status({fill: 'blue', shape: 'dot', text: 'querying'}) var now = new Date() eventsBetween(node, cal, {}, node.last, now, function (err, events) { setNextTimeout(node, cal, now, function () { node.emit('input', {}) }) if (err) { node.error(err, msg) node.status({fill: 'blue', shape: 'dot', text: 'failed'}) } else { node.status({}) events.forEach(function (ev) { sendEvent(node, ev, {}) }) } }) }) node.timeout = setNextTimeout(node, cal, new Date(), function () { node.emit('input', {}) }) node.on('close', function () { if (node.timeout !== null) { clearTimeout(node.timeout) delete node.timeout } }) }) } RED.nodes.registerType('google calendar in', GoogleCalendarInputNode) function setNextStartingTimeout (node, cal, after, cb) { node.status({fill: 'blue', shape: 'dot', text: 'querying next event'}) node.last = new Date(after.getTime()) after = new Date(after.getTime() + node.offset) // apply offset nextStartingEvent(node, cal, {}, after, function (err, ev) { var timeout = 900000 // 15 minutes node.status({}) if (!err && ev) { var start = getEventDate(ev) if (start) { timeout = Math.min(timeout, start.getTime() - after.getTime()) } } if (timeout >= 0) { node.timeout = setTimeout(cb, timeout) } else { console.log('timeout invalid') } }) } function setNextEndingTimeout (node, cal, after, cb) { node.status({fill: 'blue', shape: 'dot', text: 'querying next event'}) node.last = new Date(after.getTime()) after = new Date(after.getTime() + node.offset) // apply offset nextEndingEvent(node, cal, {}, after, function (err, ev) { var timeout = 900000 // 15 minutes node.status({}) if (!err && ev) { var end = getEventDate(ev, 'end') if (end) { timeout = Math.min(timeout, end.getTime() - after.getTime()) } } if (timeout >= 0) { node.timeout = setTimeout(cb, timeout) } else { console.log('timeout invalid') } }) } function GoogleCalendarQueryNode (n) { RED.nodes.createNode(this, n) this.google = RED.nodes.getNode(n.google) this.calendar = n.calendar || 'primary' if (!this.google || !this.google.credentials.accessToken) { this.warn(RED._('Missing google credentials')) return } var node = this node.status({fill: 'blue', shape: 'dot', text: 'querying'}) calendarList(node, function (err) { if (err) { node.error(err, {}) node.status({fill: 'red', shape: 'ring', text: 'failed'}) return } node.status({}) node.on('input', function (msg) { node.status({fill: 'blue', shape: 'dot', text: 'querying'}) var cal = calendarByNameOrId(node, msg.calendar) || calendarByNameOrId(node, node.calendar) if (!cal) { node.error(RED._('invalid calendar'), msg) node.status({fill: 'red', shape: 'ring', text: 'invalid calendar'}) return } nextStartingEvent(node, cal, msg, function (err, ev) { if (err) { node.error(RED._('Error: __error__', {error: err.toString()}), msg) node.status({fill: 'red', shape: 'ring', text: 'failed'}) return } if (!ev) { node.error(RED._('Error: no event found'), msg) node.status({fill: 'red', shape: 'ring', text: 'no event'}) } else { sendEvent(node, ev, msg) node.status({}) } }) }) }) } RED.nodes.registerType('google calendar', GoogleCalendarQueryNode) function calendarByName (node, name) { if (typeof name === 'undefined') { return null } for (var cal in node.calendars) { if (node.calendars.hasOwnProperty(cal)) { if (node.calendars[cal].summary === name) { return node.calendars[cal] } } } return null } function calendarByNameOrId (node, nameOrId) { return node.calendars.hasOwnProperty(nameOrId) ? node.calendars[nameOrId] // an id : calendarByName(node, nameOrId) // maybe a name } function calendarList (node, cb) { node.calendars = {} node.google.request('https://www.googleapis.com/calendar/v3/users/me/calendarList', function (err, data) { if (err) { cb(RED._('failed to fetch calendar list: __message__', {message: err.toString()})) return } if (data.error) { cb(RED._('failed to fetch calendar list: __message__', {message: data.error.message})) return } for (var i = 0; i < data.items.length; i++) { var cal = data.items[i] if (cal.primary) { node.calendars.primary = cal } node.calendars[cal.id] = cal } cb(null) }) } function nextStartingEvent (node, cal, msg, after, cb) { if (typeof after === 'function') { cb = after after = new Date() } var request = { url: 'https://www.googleapis.com/calendar/v3/calendars/' + cal.id + '/events' } request.qs = { maxResults: 10, orderBy: 'startTime', singleEvents: true, showDeleted: false, timeMin: after.toISOString() } if (msg.payload) { request.qs.q = RED.util.ensureString(msg.payload) } var handle_response = function (err, data) { if (err) { cb(RED._('Error: __error__', {error: err.toString()}), null) } else if (data.error) { cb(RED._('Error: __error__', {code: data.error.code, message: JSON.stringify(data.error.message)}), null) } else { var ev /* 0 - 10 events ending after now ordered by startTime * so we find the first that starts after now to * give us the "next" event */ for (var i = 0; i < data.items.length; i++) { ev = data.items[i] var start = getEventDate(ev) if (start && start.getTime() > after.getTime()) { break } ev = undefined } if (!ev && data.hasOwnProperty('nextPageToken')) { request.qs.pageToken = data.nextPageToken node.google.request(request, handle_response) } else { cb(null, ev) } } } node.google.request(request, handle_response) } function nextEndingEvent (node, cal, msg, after, cb) { if (typeof after === 'function') { cb = after after = new Date() } var request = { url: 'https://www.googleapis.com/calendar/v3/calendars/' + cal.id + '/events' } /* orderby: endTime is not permitted by API so for now this assumes * that events are not nested. * TODO: support nested events - at least simple, common cases * such as an event overlapping an all day event */ request.qs = { maxResults: 10, orderBy: 'startTime', singleEvents: true, showDeleted: false, timeMin: after.toISOString() } if (msg.payload) { request.qs.q = RED.util.ensureString(msg.payload) } var handle_response = function (err, data) { if (err) { cb(RED._('Error: __error__', {error: err.toString()}), null) } else if (data.error) { cb(RED._('Error: __error__', {code: data.error.code, message: JSON.stringify(data.error.message)}), null) } else { var ev /* 0 - 10 events ending after now ordered by startTime * so we find the first that starts after now to * give us the "next" event */ for (var i = 0; i < data.items.length; i++) { ev = data.items[i] var end = getEventDate(ev, 'end') if (end && end.getTime() > after.getTime()) { break } ev = undefined } if (!ev && data.hasOwnProperty('nextPageToken')) { request.qs.pageToken = data.nextPageToken node.google.request(request, handle_response) } else { cb(null, ev) } } } node.google.request(request, handle_response) } function eventsStartingBetween (node, cal, msg, start, end, results, cb) { if (typeof results === 'function') { cb = results results = { events: [] } } start = new Date(start.getTime() + node.offset) // apply offset end = new Date(end.getTime() + node.offset) // apply offset var request = { url: 'https://www.googleapis.com/calendar/v3/calendars/' + cal.id + '/events' } request.qs = { maxResults: 10, orderBy: 'startTime', singleEvents: true, showDeleted: false, timeMin: start.toISOString(), timeMax: (new Date(end.getTime() + 60 * 1000)).toISOString() } if (msg.payload) { request.qs.q = RED.util.ensureString(msg.payload) } if (results.hasOwnProperty('nextPageToken')) { request.qs.pageToken = results.nextPageToken } node.google.request(request, function (err, data) { if (err) { cb(RED._('Error: __error__', {error: err.toString()}), null) } else if (data.error) { cb(RED._('Error: __error__', {code: data.error.code, message: JSON.stringify(data.error.message)}), null) } else { /* 0 - 10 events ending after now ordered by startTime * so we find the first that starts after now to * give us the "next" event */ for (var i = 0; i < data.items.length; i++) { var ev = data.items[i] var evStart = getEventDate(ev) if (evStart) { if (evStart.getTime() > end.getTime()) { // timeMax should catch these break } else if (evStart.getTime() > start.getTime()) { results.events.push(ev) } } } if (data.hasOwnProperty('nextPageToken')) { results.nextPageToken = data.nextPageToken eventsStartingBetween(node, cal, msg, start, end, results, cb) } else { cb(null, results.events) } } }) } function eventsEndingBetween (node, cal, msg, start, end, results, cb) { if (typeof results === 'function') { cb = results results = { events: [] } } start = new Date(start.getTime() + node.offset) // apply offset end = new Date(end.getTime() + node.offset) // apply offset var request = { url: 'https://www.googleapis.com/calendar/v3/calendars/' + cal.id + '/events' } /* orderby: endTime is not permitted by API so for now events are * returned in startTime order rather than end time order which * would be more natural. This is probably okay for most cases. * TODO: post-process events list to order them by end time */ request.qs = { maxResults: 10, orderBy: 'startTime', // endTime is not permitted by API singleEvents: true, showDeleted: false, timeMin: start.toISOString(), timeMax: (new Date(end.getTime() + 60 * 1000)).toISOString() } if (msg.payload) { request.qs.q = RED.util.ensureString(msg.payload) } if (results.hasOwnProperty('nextPageToken')) { request.qs.pageToken = results.nextPageToken } node.google.request(request, function (err, data) { if (err) { cb(RED._('Error: __error__', {error: err.toString()}), null) } else if (data.error) { cb(RED._('Error: __error__', {code: data.error.code, message: JSON.stringify(data.error.message)}), null) } else { /* 0 - 10 events ending after now ordered by startTime * so we find the first that starts after now to * give us the "next" event */ for (var i = 0; i < data.items.length; i++) { var ev = data.items[i] var evEnd = getEventDate(ev, 'end') if (evEnd) { if (evEnd.getTime() > end.getTime()) { break } else if (evEnd.getTime() > start.getTime()) { results.events.push(ev) } } } if (data.hasOwnProperty('nextPageToken')) { results.nextPageToken = data.nextPageToken eventsEndingBetween(node, cal, msg, start, end, results, cb) } else { cb(null, results.events) } } }) } function sendEvent (node, ev, msg) { if (typeof msg === 'undefined') { msg = {} } delete msg.error var payload = msg.payload = {} if (ev.summary) { payload.title = msg.title = ev.summary } if (ev.description) { payload.description = msg.description = ev.description } else { delete msg.description } if (ev.location) { /* intentionally the same object so that * if a node modifies msg.location (for * example by looking up * msg.location.description and adding * msg.location.{lat,lon} then both copies * will be updated. */ payload.location = msg.location = { description: ev.location } } else { delete msg.location } var start = getEventDate(ev) if (start) { payload.start = start } if (ev.start && ev.start.date) { payload.allDayEvent = true } var end = getEventDate(ev, 'end') if (end) { payload.end = end } if (ev.creator) { payload.creator = { name: ev.creator.displayName, email: ev.creator.email } } if (ev.attendees) { payload.attendees = [] ev.attendees.forEach(function (a) { payload.attendees.push({ name: a.displayName, email: a.email }) }) } msg.data = ev node.send(msg) } function getEventDate (ev, type) { if (typeof type === 'undefined') { type = 'start' } if (ev[type] && ev[type].dateTime) { return new Date(ev[type].dateTime) } else if (ev.start && ev.start.date) { return new Date(ev[type].date) } else { return null } } function GoogleCalendarOutNode (n) { RED.nodes.createNode(this, n) this.google = RED.nodes.getNode(n.google) this.calendar = n.calendar || 'primary' if (!this.google || !this.google.credentials.accessToken) { this.warn(RED._('Missing google credentials')) return } var node = this node.status({fill: 'blue', shape: 'dot', text: 'querying'}) calendarList(node, function (err) { if (err) { node.error(err) node.status({fill: 'red', shape: 'ring', text: 'failed'}) return } node.status({}) node.on('input', function (msg) { node.status({fill: 'blue', shape: 'dot', text: 'creating'}) var cal = calendarByNameOrId(node, msg.calendar) || calendarByNameOrId(node, node.calendar) if (!cal) { node.error(RED._('invalid calendar'), msg) node.status({fill: 'red', shape: 'ring', text: 'invalid calendar'}) return } var request = { method: 'POST' } if (typeof msg.payload === 'object') { request.url = 'https://www.googleapis.com/calendar/v3/calendars/' + cal.id + '/events' request.body = msg.payload } else { request.url = 'https://www.googleapis.com/calendar/v3/calendars/' + cal.id + '/events/quickAdd' request.form = { text: RED.util.ensureString(msg.payload) } } if (node.sendNotifications || msg.sendNotifications) { request.query = { sendNotifications: true } } node.google.request(request, function (err, data) { if (err) { node.error(err.toString(), msg) node.status({fill: 'red', shape: 'ring', text: 'failed'}) } else if (data.error) { node.error(data.error.message, msg) node.status({fill: 'red', shape: 'ring', text: 'failed'}) } else { node.status({}) } }) }) }) } RED.nodes.registerType('google calendar out', GoogleCalendarOutNode) }