I've used the "Populate a team vacation calendar" sample at https://developers.google.com/apps-script/samples/automations/vacation-calendar to build an Apps Script to pull all Out Of Office events into a shared calendar. The script has been working fine for a few months. Without any changes to the script, now when calling Calendar.Events.import(event, TEAM_CALENDAR_ID);
I get an error:
GoogleJsonResponseException: API call to calendar.events.import failed with error: Bad Request
I've run the script in the debugger, and the error doesn't provide any insight into what the error actually is, beyond that it's a 400 Bad Request. There are lots of other Q&A relating to date/time formatting, but the event
variable I'm importing is from an earlier call to Calendar.Events.list(...)
so it's already a JS event object produced by the api itself.
Here's a minimal reproducible example:
let TEAM_CALENDAR_ID = '<calendar id goes here>';
function testImport() {
const now = new Date();
let user = Session.getActiveUser();
// Fetch next 10 events
let events = Calendar.Events.list(user.getEmail(), {
timeMin: now.toISOString(),
singleEvents: true,
orderBy: 'startTime',
maxResults: 10
});
if (events.items.length === 0) {
console.error('No events found');
return;
}
// Use next upcoming event for this user
let event = events.items[0];
// Set event fields
event.organizer = { id: TEAM_CALENDAR_ID };
event.attendees = [];
try {
Calendar.Events.import(event, TEAM_CALENDAR_ID);
} catch (e) {
console.error('Error attempting to import event: %s.', e.toString());
}
}
How do I debug this?
Here's the entire script:
// Set the ID of the team calendar to add events to. You can find the calendar's
// ID on the settings page.
let TEAM_CALENDAR_ID = '<calendar ID here>';
// Set the email address of the Google Group that contains everyone in the team.
// Ensure the group has less than 500 members to avoid timeouts.
let GROUP_EMAIL = '<group email address here>';
let MONTHS_IN_ADVANCE = 3;
/**
* Sets up the script to run automatically every hour.
*/
function setup() {
let triggers = ScriptApp.getProjectTriggers();
if (triggers.length > 0) {
throw new Error('Triggers are already setup.');
}
ScriptApp.newTrigger('sync').timeBased().everyHours(1).create();
// Runs the first sync immediately.
sync();
}
/**
* Looks through the group members' public calendars and adds any
* 'vacation' or 'out of office' events to the team calendar.
*/
function sync() {
// Defines the calendar event date range to search.
let today = new Date();
let maxDate = new Date();
maxDate.setMonth(maxDate.getMonth() + MONTHS_IN_ADVANCE);
// Determines the time the the script was last run.
let lastRun = PropertiesService.getScriptProperties().getProperty('lastRun');
lastRun = lastRun ? new Date(lastRun) : null;
// Gets the list of users in the Google Group.
let users = GroupsApp.getGroupByEmail(GROUP_EMAIL).getUsers();
// For each user, finds Out Of Office events, and import
// each to the team calendar.
let count = 0;
users.forEach(function(user) {
let events = findEvents(user, today, maxDate, lastRun);
events.forEach(function(event) {
importEvent(user, event);
count++;
});
});
PropertiesService.getScriptProperties().setProperty('lastRun', today);
console.log('Updated ' + count + ' events');
}
/**
* In a given user's calendar, looks for Out Of Office events within the
* specified date range and returns any such events found.
* @param {Session.User} user The user to retrieve events for.
* @param {Date} start The starting date of the range to examine.
* @param {Date} end The ending date of the range to examine.
* @param {Date} optSince A date indicating the last time this script was run.
* @return {Calendar.Event[]} An array of calendar events.
*/
function findEvents(user, start, end, optSince) {
let params = {
timeMin: formatDateAsRFC3339(start),
timeMax: formatDateAsRFC3339(end),
showDeleted: true,
};
if (optSince) {
// This prevents the script from examining events that have not been
// modified since the specified date (that is, the last time the
// script was run).
params.updatedMin = formatDateAsRFC3339(optSince);
}
let pageToken = null;
let events = [];
do {
params.pageToken = pageToken;
let response;
try {
response = Calendar.Events.list(user.getEmail(), params);
} catch (e) {
console.error('Error retriving events for %s: %s; skipping',
user, e.toString());
continue;
}
events = events.concat(response.items.filter(function(item) {
return shoudImportEvent(user, item);
}));
pageToken = response.nextPageToken;
} while (pageToken);
return events;
}
/**
* Determines if the given event should be imported into the shared team
* calendar.
* @param {Session.User} user The user that is attending the event.
* @param {Calendar.Event} event The event being considered.
* @return {boolean} True if the event should be imported.
*/
function shoudImportEvent(user, event) {
// Skip events that are not Out Of Office
if (event.eventType != "outOfOffice") {
return false;
}
// If the user is the creator of the event, always imports it.
if (!event.organizer || event.organizer.email == user.getEmail()) {
return true;
}
// Only imports events the user has accepted.
if (!event.attendees) {
return false;
}
let matching = event.attendees.filter(function(attendee) {
return attendee.self;
});
return matching.length > 0 && matching[0].responseStatus == 'accepted';
}
/**
* Imports the given event from the user's calendar into the shared team
* calendar.
* @param {string} username The team member that is attending the event.
* @param {Calendar.Event} event The event to import.
*/
function importEvent(user, event) {
let username = user.getEmail().split('@')[0];
username = username.charAt(0).toUpperCase() + username.slice(1);
event.summary = '[' + username + '] ' + event.summary;
event.organizer = {
id: TEAM_CALENDAR_ID,
};
event.attendees = [];
let action = event.status == "confirmed" ? "Importing" : "Removing";
console.log('%s: %s on %s', action, event.summary, event.start.getDateTime());
try {
Calendar.Events.import(event, TEAM_CALENDAR_ID);
} catch (e) {
console.error('Error attempting to import event: %s. Skipping.',
e.toString());
}
}
/**
* Returns an RFC3339 formated date String corresponding to the given
* Date object.
* @param {Date} date a Date.
* @return {string} a formatted date string.
*/
function formatDateAsRFC3339(date) {
return Utilities.formatDate(date, 'UTC', 'yyyy-MM-dd\'T\'HH:mm:ssZ');
}
function formatDateAsRFC3339(date) { return Utilities.formatDate(date, 'UTC', 'yyyy-MM-dd\'T\'HH:mm:ssZ'); }
which was found at your link above – AnthropocentricformatDateAsRFC3339
where? I'm already using it to specify the date range in my call toCalendar.Events.list(...)
– MauriceCalendar.Events.import
call. To troubleshoot, refer to the official documentation for Events: import. The document specifies the sequence asCalendar.Events.import(CALENDAR_ID, EVENT_OBJECT)
, while your current implementation usesCalendar.Events.import(EVENT_OBJECT, CALENDAR_ID)
. If the error persists, ensure that the calendar ID and the event object aren't corrupted by usingconsole.log()
. This can help verify the structure and contents for potential issues. – Rafaelofresource
(the event) comes first, andcalendarId
comes second (the full text isimport(resource: Calendar_v3.Calendar.V3.Schema.Event, calendarId: string): Calendar_v3.Calendar.V3.Schema.Event
). Also, this code has been working fine for at least a year. That said, I flipped the arguments and the same error occurred. – MauriceCalendar.Events.list(..)
and the calendarId is correct. – Mauricedelete event.outOfOfficeProperties
) does fix the problem. Thanks for linking me to the bug report. – Maurice