I'm having some serious difficulties getting a modified version (or the original version, for that matter) of this Bike Shop example to work.
I'm attempting to duplicate the basic functionality, but add fields for Name, Phone Number, Email, etc. into the calendar event.
Maybe it's because this is my first soiree with Node.js, but this is proving to be less enjoyable than bathing in hot cement.
I'll quickly summarize the main issues I've been having (edited 3/15/19):
Getting events to populate in Calendar
So I've gotten just about everything sorted out. My main issue was that my contexts were capitalized, therefore not recognized. Converting most of the intents to top-level helped as well. Now I can consistently get the fulfillment responses to fill in and give me my first confirmation message, but I always get my second 'error' response and no event is made in the calendar.
Not showing on calendar
How do I update the googleapis library? Just change the ^27 to ^30 in the package.json?
Entities with a hyphen
I removed hyphens from the names of parameters to allow for easier calling (I didn't know the name wasn't just a label), but for future reference and clarity, should an array of parameters be called using .parameters[]
, .properties[]
, or .params[]
?
And just for reference, here's my Intent flow:
Scheduleappointment >
FirstLast (gets first and last name, assigns to system entities) >
ServiceNeeded (gets service needed, assigns to developer entity) >
Date Time MeetingPlace Email PhoneN (gets date, time, phone number & email: assigns to system entities; gets location: assigns to developer entity)
My index.js:
/**
* Copyright 2017 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
const functions = require('firebase-functions');
const {google} = require('googleapis');
const {WebhookClient} = require('dialogflow-fulfillment');
// Set up Google Calendar service account credentials
const serviceAccountAuth = new google.auth.JWT({
email: serviceAccount.client_email,
key: serviceAccount.private_key,
scopes: 'https://www.googleapis.com/auth/calendar'
});
const calendar = google.calendar('v3');
process.env.DEBUG = 'dialogflow:*'; // It enables lib debugging statements
const timeZone = 'America/New_York'; // Change it to your time zone
exports.dialogflowFirebaseFulfillment = functions.https.onRequest((request, response) => {
const agent = new WebhookClient({ request, response });
// This function receives the date and time values from the context 'MakeAppointment-followup'
// and calls the createCalendarEvent() function to mark the specified time slot on Google Calendar.
function makeAppointment (agent) {
// Get the contexts
const contextF = agent.context.get('firstlast');
const contextS = agent.context.get('serviceneeded');
const contextD = agent.context.get('datetimemeetingplaceemailphonen-followup');
// This variable needs to hold an instance of Date object that specifies the start time of the appointment.
const dateTimeStart = convertTimestampToDate(contextD.parameters.date, contextD.parameters.time);
// This variable holds the end time of the appointment, which is calculated by adding an hour to the start time.
const dateTimeEnd = addHours(dateTimeStart, 1);
// Convert the Date object into human-readable strings.
const appointmentTimeString = getLocaleTimeString(dateTimeStart);
const appointmentDateString = getLocaleDateString(dateTimeStart);
// set properties to variables
const appointmentLocationString = contextD.parameters.meetingPlace;
const appointmentEmail = contextD.parameters.email;
const appointmentService = contextS.parameters.ServiceNeeded;
const appointmentFullName = contextF.parameters.givenName + " " + contextF.parameters.lastName;
const appointmentFirstName = contextF.parameters.givenName;
const appointmentPhoneString = contextD.parameters.phoneNumber;
// Delete the context 'MakeAppointment-followup'; this is the final step of the path.
agent.context.delete('datetimemeetingplaceemailphonen-followup');
// The createCalendarEvent() function checks the availability of the time slot and marks the time slot on Google Calendar if the slot is available.
return createCalendarEvent(agent, dateTimeStart, dateTimeEnd, appointmentFullName, appointmentPhoneString, appointmentLocationString, appointmentEmail, appointmentService).then(() => {
agent.context.delete('serviceneeded');
agent.context.delete('firstlast');
agent.context.delete('schedule');
agent.add(`Got it! I have your appointment scheduled on ${appointmentDateString} at ${appointmentTimeString}—we'll contact you shortly to confirm the deets! See you soon, ${appointmentFirstName}. Good-bye!`);
}).catch(() => {
agent.add(`Sorry, ${appointmentFirstName}, something went wrong—I couldn't book ${appointmentDateString} at ${appointmentTimeString}. Try trying again! If that doesn't work, let us know—Mitch probably just messed up something...`);
});
}
// This function receives the date and time values from the context 'MakeAppointment-followup'
// and calls the checkCalendarAvailablity() function to check the availability of the time slot on Google Calendar.
function checkAppointment (agent) {
// Get the contexts
const contextF = agent.context.get('firstlast');
const contextS = agent.context.get('serviceneeded');
// This variable needs to hold an instance of Date object that specifies the start time of the appointment.
const dateTimeStart = convertTimestampToDate(agent.parameters.date, agent.parameters.time);
// This variable holds the end time of the appointment, which is calculated by adding an hour to the start time.
const dateTimeEnd = addHours(dateTimeStart, 1);
// Convert the Date object into human-readable strings.
const appointmentTimeString = getLocaleTimeString(dateTimeStart);
const appointmentDateString = getLocaleDateString(dateTimeStart);
// set properties into variables
const appointmentLocationString = agent.parameters.meetingPlace;
const appointmentEmail = agent.parameters.email;
const appointmentService = contextS.parameters.ServiceNeeded;
const appointmentFullName = contextF.parameters.givenName + " " + contextF.parameters.lastName;
const appointmentFirstName = contextF.parameters.givenName;
const appointmentPhoneString = agent.parameters.phoneNumber;
// The checkCalendarAvailablity() function checks the availability of the time slot.
return checkCalendarAvailablity(dateTimeStart, dateTimeEnd).then(() => {
// The time slot is available.
// The function returns a response that asks for the confirmation of the date and time.
agent.add(`Okay, ${appointmentFullName}, so you've said that you'd like your appointment on ${appointmentDateString} at ${appointmentTimeString}. We'll call ${appointmentPhoneString} and/or email ${appointmentEmail} to confirm this appointment ${appointmentLocationString} about ${appointmentService}. Did I get that right?`);
}).catch(() => {
// The time slot is not available.
agent.add(`Sorry, ${appointmentFirstName}, we're booked up on ${appointmentDateString} at ${appointmentTimeString}. Huge bummer, I know =/ But is there another time you'd like to schedule your appointment?`);
// Delete the context 'MakeAppointment-followup' to return the flow of conversation to the beginning.
agent.context.delete('datetimemeetingplaceemailphonen-followup');
});
}
// Mapping of the functions to the agent's intents.
let intentMap = new Map();
intentMap.set('Date Time MeetingPlace Email PhoneN', checkAppointment);
intentMap.set('Date Time MeetingPlace Email PhoneN - yes', makeAppointment);
agent.handleRequest(intentMap);
});
// This function checks for the availability of the time slot, which starts at 'dateTimeStart' and ends at 'dateTimeEnd'.
// 'dateTimeStart' and 'dateTimeEnd' are instances of a Date object.
function checkCalendarAvailablity (dateTimeStart, dateTimeEnd) {
return new Promise((resolve, reject) => {
calendar.events.list({
auth: serviceAccountAuth, // List events for time period
calendarId: calendarId,
timeMin: dateTimeStart.toISOString(),
timeMax: dateTimeEnd.toISOString()
}, (err, calendarResponse) => {
// Check if there is an event already on the Calendar
if (err || calendarResponse.data.items.length > 0) {
reject(err || new Error('Requested time conflicts with another appointment'));
}else {
resolve(calendarResponse);
}
});
});
}
// This function marks the time slot on Google Calendar. The time slot on the calendar starts at 'dateTimeStart' and ends at 'dateTimeEnd'.
// 'dateTimeStart' and 'dateTimeEnd' are instances of a Date object.
function createCalendarEvent (agent, dateTimeStart, dateTimeEnd, appointmentFullName, appointmentPhoneString, appointmentLocationString, appointmentEmail, appointmentService) {
// assign values to variables
appointmentPhoneString = agent.parameters.phoneNumber;
appointmentLocationString = agent.parameters.meetingPlace;
appointmentEmail = agent.parameters.email;
appointmentService = agent.parameters.ServiceNeeded;
appointmentFullName = agent.parameters.givenName + " " + agent.parameters.lastName;
return new Promise((resolve, reject) => {
calendar.events.list({
auth: serviceAccountAuth, // List events for time period
calendarId: calendarId,
timeMin: dateTimeStart.toISOString(),
timeMax: dateTimeEnd.toISOString()
}, (err, calendarResponse) => {
// Check if there is an event already on the Calendar
if (err || calendarResponse.data.items.length > 0) {
reject(err || new Error('Requested time conflicts with another appointment'));
} else {
// Create event for the requested time period
calendar.events.insert({ auth: serviceAccountAuth,
calendarId: calendarId,
resource: {
summary: 'Appsoft Appointment',
start: {
dateTime: dateTimeStart
},
end: {
dateTime: dateTimeEnd
},
attendees:[ {
displayName: appointmentFullName,
email: appointmentEmail,
}],
location: appointmentLocationString,
description: 'Phone Number: ' + appointmentPhoneString + '; Service Needed: ' + appointmentService}
}, (err, event) => {
err ? reject(err) : resolve(event);
}
);
}
});
});
}
// A helper function that receives Dialogflow's 'date' and 'time' parameters and creates a Date instance.
function convertTimestampToDate(date, time){
// Parse the date, time, and time zone offset values from the input parameters and create a new Date object
return new Date(Date.parse(date.split('T')[0] + 'T' + time.split('T')[1].split('-')[0] + '-' + time.split('T')[1].split('-')[1]));
}
// A helper function that adds the integer value of 'hoursToAdd' to the Date instance 'dateObj' and returns a new Data instance.
function addHours(dateObj, hoursToAdd){
return new Date(new Date(dateObj).setHours(dateObj.getHours() + hoursToAdd));
}
// A helper function that converts the Date instance 'dateObj' into a string that represents this time in English.
function getLocaleTimeString(dateObj){
return dateObj.toLocaleTimeString('en-US', { hour: 'numeric', hour12: true, timeZone: timeZone });
}
// A helper function that converts the Date instance 'dateObj' into a string that represents this date in English.
function getLocaleDateString(dateObj){
return dateObj.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', timeZone: timeZone });
}
My package.json:
{
"name": "DialogflowFirebaseWebhook",
"description": "Firebase Webhook dependencies for a Dialogflow agent.",
"version": "0.0.1",
"private": true,
"license": "Apache Version 2.0",
"author": "Google Inc.",
"engines": {
"node": "6"
},
"scripts": {
"lint": "semistandard --fix \"**/*.js\"",
"start": "firebase deploy --only functions",
"deploy": "firebase deploy --only functions"
},
"dependencies": {
"firebase-functions": "^2.0.2",
"firebase-admin": "^5.13.1",
"googleapis": "^27.0.0",
"actions-on-google": "2.2.0",
"dialogflow-fulfillment": "0.6.1"
}
}
Thanks in advance for your help!