/* global chrome */ const gapi_base = 'https://www.googleapis.com/calendar/v3'; const GApiError = { invalidSyncToken: 1, otherError: 2, }; function to_params(dict) { return Object.entries(dict).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join('&'); } function getAuthToken() { return new Promise(resolver => chrome.identity.getAuthToken( {interactive: true}, token => resolver(token))); } function getCalendars(token) { return fetch(`${gapi_base}/users/me/calendarList?${to_params({access_token: token})}`, { method: 'GET', async: true }) .then(response => response.json()) .then(data => data.items); } function getColors(token) { return fetch(`${gapi_base}/colors?${to_params({access_token: token})}`, { method: 'GET', async: true }) .then(response => response.json()); } function getEvent(calId, eventId, token) { return fetch(`${gapi_base}/calendars/${calId}/events/${eventId}?${to_params({access_token: token})}`, { method: 'GET', async: true }) .then(response => response.json()); } function getEvents(calId, token, syncToken, resultsPerRequest=100) { let results = []; const singleFetch = (pageToken, syncToken) => fetch(`${gapi_base}/calendars/${calId}/events?${to_params({ access_token: token, pageToken, syncToken, maxResults: resultsPerRequest })}`, { method: 'GET', async: true }) .then(response => { if (response.status === 200) return response.json(); else if (response.status == 410) throw GApiError.invalidSyncToken; else throw GApiError.otherErrors; }) .then(data => { results.push(...data.items); if (data.nextPageToken) { return singleFetch(data.nextPageToken, ''); } else { return ({ nextSyncToken: data.nextSyncToken, results }); } }) return singleFetch('', syncToken); } class GCalendar { constructor(calId, name) { this.calId = calId; this.name = name; this.token = getAuthToken(); this.syncToken = ''; this.cache = {}; } static dateToCacheKey(date) { return Math.floor(date / 8.64e7); } getSlot(k) { if (!this.cache[k]) this.cache[k] = {}; return this.cache[k]; } static slotStartDate(k) { return new Date(k * 8.64e7); } static slotEndDate(k) { return new Date((k + 1) * 8.64e7); } addEvent(e) { let ks = GCalendar.dateToCacheKey(e.start); let ke = GCalendar.dateToCacheKey(new Date(e.end.getTime() - 1)); if (ks === ke) this.getSlot(ks)[e.id] = { start: e.start, end: e.end, id: e.id, summary: e.summary}; else { this.getSlot(ks)[e.id] = { start: e.start, end: GCalendar.slotEndDate(ks), id: e.id, summary: e.summary}; this.getSlot(ke)[e.id] = { start: GCalendar.slotStartDate(ke), end: e.end, id: e.id, summary: e.summary}; for (let k = ks + 1; k < ke; k++) this.getSlot(k)[e.id] = { start: GCalendar.slotStartDate(k), end: GCalendar.slotEndDate(k), id: e.id, summary: e.summary}; } } removeEvent(e) { let ks = GCalendar.dateToCacheKey(e.start); let ke = GCalendar.dateToCacheKey(new Date(e.end.getTime() - 1)); for (let k = ks; k <= ke; k++) delete this.getSlot(k)[e.id]; } getSlotEvents(k, start, end) { let s = this.getSlot(k); let results = []; for (let id in s) { if (!(s[id].start >= end || s[id].end <= start)) { results.push({ id, start: s[id].start < start ? start: s[id].start, end: s[id].end > end ? end: s[id].end, summary: s[id].summary }); } } return results; } getCachedEvents(start, end) { let ks = GCalendar.dateToCacheKey(start); let ke = GCalendar.dateToCacheKey(new Date(end.getTime() - 1)); let results = this.getSlotEvents(ks, start, end); for (let k = ks + 1; k < ke; k++) { let s = this.getSlot(k); for (let id in s) results.push(s[id]); } if (ke > ks) results.push(...this.getSlotEvents(ke, start, end)); return results; } sync() { return this.token.then(token => getEvents(this.calId, token, this.syncToken).then(r => { this.syncToken = r.nextSyncToken; let pm_results = r.results.map(e => e.start ? Promise.resolve(e) : getEvent(this.calId, e.id, token)); return Promise.all(pm_results).then(results => results.forEach(e => { e.start = new Date(e.start.dateTime); e.end = new Date(e.end.dateTime); if (e.status === 'confirmed') this.addEvent(e); else if (e.status === 'cancelled') this.removeEvent(e); })); })).catch(e => { if (e == GApiError.invalidSyncToken) { this.syncToken = ''; this.sync(); } else throw e; }); } getEvents(start, end) { return this.sync().then(() => this.getCachedEvents(start, end)); } }