From 601f10a3cf947c4fa9a110777b957cd49d9c009d Mon Sep 17 00:00:00 2001 From: Mark Hindess Date: Thu, 20 Nov 2014 10:04:56 +0000 Subject: [PATCH 1/8] Refactor sending a calendar event message. --- google/calendar.js | 307 ++++++++++++++++++++++++--------------------- 1 file changed, 165 insertions(+), 142 deletions(-) diff --git a/google/calendar.js b/google/calendar.js index 1007509e..444a56c7 100644 --- a/google/calendar.js +++ b/google/calendar.js @@ -22,7 +22,6 @@ module.exports = function(RED) { this.google = RED.nodes.getNode(n.google); this.calendar = n.calendar || 'primary'; - this.calendars = {}; if (!this.google || !this.google.credentials.accessToken) { this.warn("Missing google credentials"); return; @@ -30,124 +29,40 @@ module.exports = function(RED) { var node = this; node.status({fill:"blue",shape:"dot",text:"querying"}); - this.google.request('https://www.googleapis.com/calendar/v3/users/me/calendarList', function(err, data) { + calendarList(node, function(err) { if (err) { - node.error("failed to fetch calendar list: " + err.toString()); + node.error(err); node.status({fill:"red",shape:"ring",text:"failed"}); return; } - if (data.error) { - node.error("failed to fetch calendar list: " + - data.error.message); - node.status({fill:"red",shape:"ring",text:"failed"}); - 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; - } node.status({}); node.on('input', function(msg) { node.status({fill:"blue",shape:"dot",text:"querying"}); - var cal = node.calendars[msg.calendar] || node.calendarByName(msg.calendar) || node.calendars[node.calendar] || node.calendarByName(node.calendar); + var cal = calendarByNameOrId(node, msg.calendar) || + calendarByNameOrId(node, node.calendar); if (!cal) { node.status({fill:"red",shape:"ring",text:"invalid calendar"}); return; } - var request = { - url: 'https://www.googleapis.com/calendar/v3/calendars/'+cal.id+'/events', - }; - var now = new Date(); - request.qs = { - maxResults: 10, - orderBy: 'startTime', - singleEvents: true, - showDeleted: false, - timeMin: now.toISOString() - }; - if (msg.payload) { - request.qs.q = RED.util.ensureString(msg.payload); - } - node.google.request(request, function(err, data) { + nextEvent(node, cal, msg, function(err, ev) { if (err) { node.error("Error: " + err.toString()); node.status({fill:"red",shape:"ring",text:"failed"}); - } else if (data.error) { - node.error("Error " + data.error.code + ": " + - JSON.stringify(data.error.message)); - node.status({fill:"red",shape:"ring",text:"failed"}); - } else { - var payload = msg.payload = {}; - 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 now.getTime()) { - payload.start = start; - break; - } - ev = undefined; - } - if (!ev) { - delete msg.data; - node.send(msg); - node.status({fill:"red",shape:"ring",text:"no event"}); - return; - } - 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; - } - 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; + delete msg.payload; + delete msg.data; + msg.error = "event lookup failed"; + node.send(msg); + return; + } + if (!ev) { + delete msg.payload; + delete msg.data; + msg.error = "no event found"; node.send(msg); + node.status({fill:"red",shape:"ring",text:"no event"}); + } else { + sendEvent(node, ev); node.status({}); } }); @@ -156,6 +71,149 @@ module.exports = function(RED) { } 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("failed to fetch calendar list: " + err.toString()); + return; + } + if (data.error) { + cb("failed to fetch calendar list: " + 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 nextEvent(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); + } + node.google.request(request, function(err, data) { + if (err) { + cb("Error: " + err.toString(), null); + } else if (data.error) { + cb("Error " + data.error.code + ": " + + 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 after.getTime()) { + break; + } + ev = undefined; + } + cb(null, ev); + } + }); + } + + function sendEvent(node, ev, msg) { + if (typeof msg === 'undefined') { + msg = {}; + } + 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'; @@ -169,23 +227,11 @@ module.exports = function(RED) { } } - GoogleCalendarQueryNode.prototype.calendarByName = function(name) { - for (var cal in this.calendars) { - if (this.calendars.hasOwnProperty(cal)) { - if (this.calendars[cal].summary === name) { - return this.calendars[cal]; - } - } - } - return; - }; - function GoogleCalendarOutNode(n) { RED.nodes.createNode(this,n); this.google = RED.nodes.getNode(n.google); this.calendar = n.calendar || 'primary'; - this.calendars = {}; if (!this.google || !this.google.credentials.accessToken) { this.warn("Missing google credentials"); return; @@ -193,30 +239,18 @@ module.exports = function(RED) { var node = this; node.status({fill:"blue",shape:"dot",text:"querying"}); - this.google.request('https://www.googleapis.com/calendar/v3/users/me/calendarList', function(err, data) { + calendarList(node, function(err) { if (err) { - node.error("failed to fetch calendar list: " + err.toString()); - node.status({fill:"red",shape:"ring",text:"failed"}); - return; - } - if (data.error) { - node.error("failed to fetch calendar list: " + - data.error.message); + node.error(err); node.status({fill:"red",shape:"ring",text:"failed"}); 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; - } node.status({}); node.on('input', function(msg) { node.status({fill:"blue",shape:"dot",text:"creating"}); - var cal = node.calendars[msg.calendar] || node.calendarByName(msg.calendar) || node.calendars[node.calendar]; + var cal = calendarByNameOrId(node, msg.calendar) || + calendarByNameOrId(node, node.calendar); if (!cal) { node.status({fill:"red",shape:"ring",text:"invalid calendar"}); return; @@ -253,15 +287,4 @@ module.exports = function(RED) { }); } RED.nodes.registerType("google calendar out", GoogleCalendarOutNode); - - GoogleCalendarOutNode.prototype.calendarByName = function(name) { - for (var cal in this.calendars) { - if (this.calendars.hasOwnProperty(cal)) { - if (this.calendars[cal].summary === name) { - return this.calendars[cal]; - } - } - } - return; - }; } From 7066ae4ffc404441c288a28bea2d23ca892ea7c5 Mon Sep 17 00:00:00 2001 From: Mark Hindess Date: Tue, 25 Nov 2014 14:17:37 +0000 Subject: [PATCH 2/8] Add Google Calendar node to inject messages for calendar events. --- google/calendar.html | 57 ++++++++++++++++ google/calendar.js | 157 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 205 insertions(+), 9 deletions(-) diff --git a/google/calendar.html b/google/calendar.html index b8360fc0..8444fa78 100644 --- a/google/calendar.html +++ b/google/calendar.html @@ -14,6 +14,63 @@ limitations under the License. --> + + + + + + diff --git a/google/calendar.js b/google/calendar.js index d44742b1..504469ca 100644 --- a/google/calendar.js +++ b/google/calendar.js @@ -20,11 +20,23 @@ module.exports = function(RED) { function GoogleCalendarInputNode(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("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 node = this; node.status({fill:"blue",shape:"dot",text:"querying"}); @@ -74,6 +86,7 @@ module.exports = function(RED) { function setNextTimeout(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 nextEvent(node, cal, {}, after, function(err, ev) { var timeout = 900000; // 15 minutes node.status({}); @@ -233,6 +246,8 @@ module.exports = function(RED) { 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' }; From 891ee0eb1e1b000478d007ce97f714e5f722eb70 Mon Sep 17 00:00:00 2001 From: Mark Hindess Date: Wed, 3 Dec 2014 09:43:41 +0000 Subject: [PATCH 5/8] Implement triggering relative to end time. --- google/calendar.html | 2 +- google/calendar.js | 147 +++++++++++++++++++++++++++++-- test/google/calendar_spec.js | 164 +++++++++++++++++++++++++++++++++++ 3 files changed, 306 insertions(+), 7 deletions(-) diff --git a/google/calendar.html b/google/calendar.html index 5fcfcd93..05bd147c 100644 --- a/google/calendar.html +++ b/google/calendar.html @@ -33,7 +33,7 @@ the of each event. diff --git a/google/calendar.js b/google/calendar.js index 504469ca..56b0b8ed 100644 --- a/google/calendar.js +++ b/google/calendar.js @@ -38,6 +38,15 @@ module.exports = function(RED) { 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) { @@ -83,11 +92,11 @@ module.exports = function(RED) { } RED.nodes.registerType("google calendar in", GoogleCalendarInputNode); - function setNextTimeout(node, cal, after, cb) { + 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 - nextEvent(node, cal, {}, after, function(err, ev) { + nextStartingEvent(node, cal, {}, after, function(err, ev) { var timeout = 900000; // 15 minutes node.status({}); if (!err && ev) { @@ -105,6 +114,28 @@ module.exports = function(RED) { }); } + 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); @@ -133,7 +164,7 @@ module.exports = function(RED) { node.status({fill:"red",shape:"ring",text:"invalid calendar"}); return; } - nextEvent(node, cal, msg, function(err, ev) { + nextStartingEvent(node, cal, msg, function(err, ev) { if (err) { node.error("Error: " + err.toString()); node.status({fill:"red",shape:"ring",text:"failed"}); @@ -195,7 +226,7 @@ module.exports = function(RED) { }); } - function nextEvent(node, cal, msg, after, cb) { + function nextStartingEvent(node, cal, msg, after, cb) { if (typeof after === 'function') { cb = after; after = new Date(); @@ -239,7 +270,51 @@ module.exports = function(RED) { }); } - function eventsBetween(node, cal, msg, start, end, results, cb) { + 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' + }; + request.qs = { + maxResults: 10, + orderBy: 'startTime', // endTime is not permitted by API + singleEvents: true, + showDeleted: false, + timeMin: after.toISOString() + }; + if (msg.payload) { + request.qs.q = RED.util.ensureString(msg.payload); + } + node.google.request(request, function(err, data) { + if (err) { + cb("Error: " + err.toString(), null); + } else if (data.error) { + cb("Error " + data.error.code + ": " + + 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 after.getTime()) { + break; + } + ev = undefined; + } + cb(null, ev); + } + }); + } + + function eventsStartingBetween(node, cal, msg, start, end, results, cb) { if (typeof results === 'function') { cb = results; results = { @@ -292,7 +367,67 @@ module.exports = function(RED) { } if (!ev && data.nextPageToken) { results.nextPageToken = data.nextPageToken; - eventsBetween(node, cal, msg, start, end, results, cb); + 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' + }; + 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.nextPageToken) { + request.qs.pageToken = results.nextPageToken; + } + node.google.request(request, function(err, data) { + if (err) { + cb("Error: " + err.toString(), null); + } else if (data.error) { + cb("Error " + data.error.code + ": " + + 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 evEnd = getEventDate(ev, 'end'); + if (evEnd) { + if (evEnd.getTime() > end.getTime()) { + break; + } else if (evEnd.getTime() > start.getTime()) { + results.events.push(ev); + } + } + ev = undefined; + } + if (!ev && data.nextPageToken) { + results.nextPageToken = data.nextPageToken; + eventsEndingBetween(node, cal, msg, start, end, results, cb); } else { cb(null, results.events); } diff --git a/test/google/calendar_spec.js b/test/google/calendar_spec.js index 959f4607..0283c7e9 100644 --- a/test/google/calendar_spec.js +++ b/test/google/calendar_spec.js @@ -209,6 +209,170 @@ describe('google calendar nodes', function() { }); }); }); + + it('injects message for calendar entry based on end time', function(done) { + var oneMinuteAgo = TimeOffset(-60); // definitely passed + var now = TimeOffset(); + var oneSecondFromNow = TimeOffset(1); + var oneMinuteFromNow = TimeOffset(60); + var oneMinuteOneSecondFromNow = TimeOffset(61); + var twoMinutesFromNow = TimeOffset(120); + var scope = nock('https://www.googleapis.com:443') + .filteringPath(function(path) { + path = path.replace(/\.\d\d\dZ$/g, '.000Z'); + path = + path.replace( + 'timeMin=' + +encodeURIComponent(oneMinuteAgo.toISOString()), + 'timeMin=oneMinuteAgo'); + [now, oneSecondFromNow].forEach(function(t) { + path = + path.replace( + 'timeMin='+ + encodeURIComponent(t.toISOString()) + .replace(/\.\d\d\dZ$/, '.000Z'), + 'timeMin=now'); + }); + [oneMinuteFromNow, oneMinuteOneSecondFromNow].forEach(function(t) { + path = + path.replace( + 'timeMax='+encodeURIComponent(t.toISOString()) + .replace(/\.\d\d\dZ$/, '.000Z'), + 'timeMax=oneMinuteFromNow'); + }); + return path; + }) + .get('/calendar/v3/users/me/calendarList') + .reply(200, { + kind : "calendar#calendarList", + items : [ + { id: "bob", summary: "Bob", primary: true }, + { id: "work", summary: "Work" }, + { id: "home", summary: "Home" } + ] + }, { + date: 'Tue, 11 Nov 2014 10:53:24 GMT', + 'content-type': 'application/json; charset=UTF-8' + }) + .get('/calendar/v3/calendars/bob/events?maxResults=10&orderBy=startTime&singleEvents=true&showDeleted=false&timeMin=now') + .reply(200, { + kind: "calendar#events", + items: [ + { + creator: { + email: "foo@example.com", + self: true, + displayName: "Bob Foo" + }, + status: "confirmed", + kind: "calendar#event", + summary: "Coffee", + attendees: [ + { + email: "foo@example.com", + responseStatus: "needsAction", + organizer: true, + self: true, + displayName: "Bob Foo" + } + ], + start: { + dateTime: oneMinuteFromNow.toISOString() + }, + end: { + dateTime: twoMinutesFromNow.toISOString() + } + } + ] + }, { + date: 'Tue, 11 Nov 2014 10:53:24 GMT', + 'content-type': 'application/json; charset=UTF-8' + }) + .get('/calendar/v3/calendars/bob/events?maxResults=10&orderBy=startTime&singleEvents=true&showDeleted=false&timeMin=oneMinuteAgo&timeMax=oneMinuteFromNow') + .reply(200, { + kind: "calendar#events", + items: [ + { + creator: { + email: "foo@example.com", + self: true, + displayName: "Bob Foo" + }, + status: "confirmed", + kind: "calendar#event", + summary: "Meeting", + attendees: [ + { + email: "foo@example.com", + responseStatus: "needsAction", + organizer: true, + self: true, + displayName: "Bob Foo" + } + ], + start: { + dateTime: oneMinuteAgo.toISOString() + }, + end: { + dateTime: now.toISOString() + } + } + ] + }, { + date: 'Tue, 11 Nov 2014 10:53:24 GMT', + 'content-type': 'application/json; charset=UTF-8' + }) + .get('/calendar/v3/calendars/bob/events?maxResults=10&orderBy=startTime&singleEvents=true&showDeleted=false&timeMin=now') + .reply(200, { + kind: "calendar#events", + items: [] + }, { + date: 'Tue, 11 Nov 2014 10:53:24 GMT', + 'content-type': 'application/json; charset=UTF-8' + }); + helper.load([googleNode, calendarNode], [ + {id:"google-config", type:"google-credentials", + displayName: "Bob"}, + {id:"calendar", type:"google calendar in", + offsetFrom: "end", + google: "google-config", wires:[["output"]]}, + {id:"output", type:"helper"} + ], { + "google-config": { + clientId: "id", + clientSecret: "secret", + accessToken: "access", + refreshToken: "refresh", + expireTime: 1000+(new Date().getTime()/1000), + displayName: "Bob" + }, + }, function() { + var calendar = helper.getNode("calendar"); + calendar.should.have.property('id', 'calendar'); + var output = helper.getNode("output"); + output.should.have.property('id', 'output'); + output.on("input", function(msg) { + msg.should.have.property('title', 'Meeting'); + scope.isDone(); + done(); + }); + + // wait for calendar.status({}) to be called twice + var count = 0; + var stub = sinon.stub(calendar, 'status', function(status) { + if (Object.getOwnPropertyNames(status).length === 0) { + count++; + if (count == 2) { + stub.restore(); + // hack last check time back a minute + calendar.last = oneMinuteAgo; + calendar.should.have.property('timeout'); + calendar.emit('input',{}); + } + } + }); + }); + }); }); describe('query node', function() { From 8bff994a99ab9dcbc3b76f6e49fe7af4bbdbca5d Mon Sep 17 00:00:00 2001 From: Mark Hindess Date: Thu, 4 Dec 2014 12:50:40 +0000 Subject: [PATCH 6/8] Improving result paging logic. --- google/calendar.js | 40 ++++++++++++++++++++++++---------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/google/calendar.js b/google/calendar.js index 56b0b8ed..e6075b86 100644 --- a/google/calendar.js +++ b/google/calendar.js @@ -245,7 +245,7 @@ module.exports = function(RED) { if (msg.payload) { request.qs.q = RED.util.ensureString(msg.payload); } - node.google.request(request, function(err, data) { + var handle_response = function(err, data) { if (err) { cb("Error: " + err.toString(), null); } else if (data.error) { @@ -265,9 +265,15 @@ module.exports = function(RED) { } ev = undefined; } - cb(null, ev); + 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) { @@ -289,7 +295,7 @@ module.exports = function(RED) { if (msg.payload) { request.qs.q = RED.util.ensureString(msg.payload); } - node.google.request(request, function(err, data) { + var handle_response = function(err, data) { if (err) { cb("Error: " + err.toString(), null); } else if (data.error) { @@ -309,9 +315,15 @@ module.exports = function(RED) { } ev = undefined; } - cb(null, ev); + 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) { @@ -337,7 +349,7 @@ module.exports = function(RED) { if (msg.payload) { request.qs.q = RED.util.ensureString(msg.payload); } - if (results.nextPageToken) { + if (results.hasOwnProperty('nextPageToken')) { request.qs.pageToken = results.nextPageToken; } node.google.request(request, function(err, data) { @@ -347,13 +359,12 @@ module.exports = function(RED) { cb("Error " + data.error.code + ": " + 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 ev = data.items[i]; var evStart = getEventDate(ev); if (evStart) { if (evStart.getTime() > end.getTime()) { @@ -363,9 +374,8 @@ module.exports = function(RED) { results.events.push(ev); } } - ev = undefined; } - if (!ev && data.nextPageToken) { + if (data.hasOwnProperty('nextPageToken')) { results.nextPageToken = data.nextPageToken; eventsStartingBetween(node, cal, msg, start, end, results, cb); } else { @@ -398,7 +408,7 @@ module.exports = function(RED) { if (msg.payload) { request.qs.q = RED.util.ensureString(msg.payload); } - if (results.nextPageToken) { + if (results.hasOwnProperty('nextPageToken')) { request.qs.pageToken = results.nextPageToken; } node.google.request(request, function(err, data) { @@ -408,13 +418,12 @@ module.exports = function(RED) { cb("Error " + data.error.code + ": " + 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 ev = data.items[i]; var evEnd = getEventDate(ev, 'end'); if (evEnd) { if (evEnd.getTime() > end.getTime()) { @@ -423,9 +432,8 @@ module.exports = function(RED) { results.events.push(ev); } } - ev = undefined; } - if (!ev && data.nextPageToken) { + if (data.hasOwnProperty('nextPageToken')) { results.nextPageToken = data.nextPageToken; eventsEndingBetween(node, cal, msg, start, end, results, cb); } else { From d201b7e21240189d358bb619230ca9b7299381cb Mon Sep 17 00:00:00 2001 From: Mark Hindess Date: Thu, 4 Dec 2014 14:40:18 +0000 Subject: [PATCH 7/8] Test looking up calendar by name. --- test/google/calendar_spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/google/calendar_spec.js b/test/google/calendar_spec.js index 0283c7e9..477df387 100644 --- a/test/google/calendar_spec.js +++ b/test/google/calendar_spec.js @@ -582,7 +582,7 @@ describe('google calendar nodes', function() { date: 'Tue, 11 Nov 2014 10:53:24 GMT', 'content-type': 'application/json; charset=UTF-8' }) - .post('/calendar/v3/calendars/bob/events', { + .post('/calendar/v3/calendars/work/events', { start: { dateTime: "2014-11-12T11:00:00Z" }, end: { dateTime: "2014-11-12T12:00:00Z" }, location: "Starbucks", @@ -611,7 +611,7 @@ describe('google calendar nodes', function() { helper.load([googleNode, calendarNode], [ {id:"google-config", type:"google-credentials", displayName: "Bob"}, - {id:"calendar", type:"google calendar out", + {id:"calendar", type:"google calendar out", calendar: "Work", google: "google-config"} ], { "google-config": { From a829fc0c3fe3b50b3c7a70ed9c8ae81707734b7e Mon Sep 17 00:00:00 2001 From: Mark Hindess Date: Mon, 8 Dec 2014 14:18:33 +0000 Subject: [PATCH 8/8] Add TODO items. --- google/calendar.js | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/google/calendar.js b/google/calendar.js index e6075b86..696b2c90 100644 --- a/google/calendar.js +++ b/google/calendar.js @@ -285,9 +285,14 @@ module.exports = function(RED) { 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', // endTime is not permitted by API + orderBy: 'startTime', singleEvents: true, showDeleted: false, timeMin: after.toISOString() @@ -397,6 +402,11 @@ module.exports = function(RED) { 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