aboutsummaryrefslogtreecommitdiff
path: root/public
diff options
context:
space:
mode:
Diffstat (limited to 'public')
-rw-r--r--public/gapi.js182
1 files changed, 182 insertions, 0 deletions
diff --git a/public/gapi.js b/public/gapi.js
new file mode 100644
index 0000000..f5ab73e
--- /dev/null
+++ b/public/gapi.js
@@ -0,0 +1,182 @@
+/* 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));
+ }
+}