import * as gapi from './gapi'; import { MsgType, Msg } from './msg'; import { Duration, TrackedPeriod, TrackedPeriodFlat } from './duration'; import moment from 'moment'; import { GraphData, getGraphData } from './graph'; import { PatternEntry, PatternEntryFlat } from './pattern'; let mainPatterns: PatternEntry[] = []; let analyzePatterns: PatternEntry[] = []; let calendars: {[id: string]: gapi.GCalendarMeta} = {}; let calData: {[id: string]: gapi.GCalendar} = {}; let config = { trackedPeriods: [ new TrackedPeriod('Today', Duration.days(1), Duration.days(0)), new TrackedPeriod('Yesterday', Duration.days(2), Duration.days(1)), new TrackedPeriod('This Week', Duration.weeks(1), Duration.weeks(0)), new TrackedPeriod('This Month', Duration.months(1), Duration.months(0))] as TrackedPeriod[], overrideNewTab: false }; let mainGraphData: GraphData[] = []; let dirtyMetadata = false; let dirtyCalData = false; let loadPromise: Promise = null; let auth = new gapi.Auth(); enum ChromeError { storageGetError = "storageGetError", storageSetError = "storageSetError" } const chromeStorageGet = (keys: string[]): Promise => ( new Promise(resolver => chrome.storage.local.get(keys, items => { if (chrome.runtime.lastError) throw ChromeError.storageGetError; resolver(items); })) ); const chromeStorageSet = (obj: {[key: string]: any}): Promise => ( new Promise(resolver => chrome.storage.local.set(obj, () => { if (chrome.runtime.lastError) throw ChromeError.storageSetError; resolver(); })) ); async function loadMetadata() { try { let items = await chromeStorageGet(['calendars', 'config', 'mainPatterns', 'analyzePatterns']); if (!items.hasOwnProperty('config')) console.log("no saved metadata"); else { config = { trackedPeriods: items.config.trackedPeriods.map((p: TrackedPeriodFlat) => TrackedPeriod.inflate(p)), overrideNewTab: items.config.overrideNewTab }; calendars = items.calendars; mainPatterns = items.mainPatterns.map((p: PatternEntryFlat) => PatternEntry.inflate(p)); analyzePatterns = items.analyzePatterns.map((p: PatternEntryFlat) => PatternEntry.inflate(p)); console.log('metadata loaded'); } } catch (_) { console.error("error while loading saved metadata"); } } async function saveMetadata() { await chromeStorageSet({ calendars, config: { trackedPeriods: config.trackedPeriods.map(p => p.deflate()), overrideNewTab: config.overrideNewTab }, mainPatterns: mainPatterns.map(p => p.deflate()), analyzePatterns: analyzePatterns.map(p => p.deflate()) }); console.log('metadata saved'); } async function loadCachedCals() { try { let items = await chromeStorageGet(['calData']); if (!items.hasOwnProperty('calData')) console.log("no cached cals"); else { let calDataFlat: {[id: string]: gapi.GCalendarFlat} = items.calData; console.log(calDataFlat); for (let id in calDataFlat) { calData[id] = gapi.GCalendar.inflate(calDataFlat[id], auth); } console.log("cached cals loaded"); } } catch (e) { console.log(e); console.error("error while loading cached cals"); } } async function saveCachedCals() { let calDataFlat: {[id: string]: gapi.GCalendarFlat} = {}; for (let id in calData) { if (!(calendars.hasOwnProperty(id) && calendars[id].enabled)) continue; calDataFlat[id] = calData[id].deflate(); } try { await chromeStorageSet({ calData: calDataFlat }); console.log('cached data saved'); } catch (_) { console.log("failed to save cached data"); } } function getCalData(id: string) { if (!calData.hasOwnProperty(id)) calData[id] = new gapi.GCalendar(id, calendars[id].name, auth); return calData[id]; } function handleGApiError(id: string, err: gapi.GApiError) { if (err === gapi.GApiError.fetchError) { console.log(`${id}: fetch error`); } else if (err === gapi.GApiError.invalidAuthToken) { console.log(`${id}: invalid auth token`); calendars[id].enabled = false; } else if (err === gapi.GApiError.notLoggedIn) { console.log(`${id}: not logged in`); } else { console.log(`${id}: ${err}`); calendars[id].enabled = false; } } async function getCalEvents(id: string, start: Date, end: Date) { let gcal = getCalData(id); try { let res = await gcal.getEvents(new Date(start), new Date(end)); dirtyCalData = res.changed; return res.events; } catch(err) { handleGApiError(id, err); console.log(`cannot load calendar ${id}`); return []; } } function updateMainGraphData() { console.log('refreshing graph data'); console.log(mainGraphData); let pms = []; for (let i = 0; i < config.trackedPeriods.length; i++) { let p = config.trackedPeriods[i]; let startD = p.start.toMoment(); let endD = p.end.toMoment(); if (!(startD && endD)) return; let start = moment().add(1, 'days').startOf('day'); if (endD.valueOf() == 0) { switch (p.start.unit) { case 'days': start = moment().add(1, 'days').startOf('day'); break; case 'weeks': start = moment().add(1, 'weeks').startOf('isoWeek'); break; case 'months': start = moment().add(1, 'months').startOf('month'); break; default: } } let end = start.clone(); start.subtract(startD); end.subtract(endD); pms.push(getGraphData( start.toDate(), end.toDate(), mainPatterns, calendars, getCalEvents ).then(r => { mainGraphData[i] = { name: p.name, start: start.toDate(), end: end.toDate(), data: r.patternGraphData }; })); } return Promise.all(pms); } async function pollSync() { console.log('poll'); /* sync all enabled calendars */ let pms = []; for (let id in calendars) { if (!calendars[id].enabled) continue; pms.push(getCalData(id).sync().catch(err => { handleGApiError(id, err); console.log(`cannot sync calendar ${id}`); })); } await Promise.all(pms); /* update the tracked graph data */ await updateMainGraphData(); pms = []; /* save the storage if state is changed */ if (dirtyMetadata) pms.push(saveMetadata().then(() => dirtyMetadata = false)); if (dirtyCalData) pms.push(saveCachedCals().then(() => dirtyCalData = false)); await Promise.all(pms); /* setup the next loop */ return new Promise(resolver => ( window.setTimeout(() => { resolver(); pollSync();}, 10000) )); } function handleMsg(port: chrome.runtime.Port) { console.assert(port.name == 'main'); port.onMessage.addListener(_msg => { let msg = Msg.inflate(_msg); console.log(msg); switch (msg.opt) { case MsgType.updatePatterns: { let patterns = msg.data.patterns.map((p: PatternEntryFlat) => PatternEntry.inflate(p)); if (msg.data.id == 'analyze') analyzePatterns = patterns; else mainPatterns = patterns; dirtyMetadata = true; port.postMessage(msg.genResp(null)); break; } case MsgType.getPatterns: { let patterns; if (msg.data.id == 'analyze') patterns = analyzePatterns; else patterns = mainPatterns; port.postMessage(msg.genResp(patterns.map(p => p.deflate()))); break; } case MsgType.updateCalendars: { calendars = msg.data; dirtyMetadata = true; port.postMessage(msg.genResp(null)); break; } case MsgType.getCalendars: { let cals = calendars; if (msg.data.enabledOnly) { cals = Object.keys(calendars) .filter(id => calendars[id].enabled) .reduce((res, id) => (res[id] = calendars[id], res), {} as {[id: string]: gapi.GCalendarMeta}); } port.postMessage(msg.genResp(cals)); break; } case MsgType.getCalEvents: { getCalEvents(msg.data.id, new Date(msg.data.start), new Date(msg.data.end)).then(data => { console.log(data); let resp = msg.genResp(data.map(e => e.deflate())); console.log(resp); port.postMessage(resp); }); break; } case MsgType.updateConfig: { for (let prop in msg.data) { if (prop === 'trackedPeriods') { config.trackedPeriods = msg.data.trackedPeriods.map((p: TrackedPeriodFlat) => TrackedPeriod.inflate(p)); } else if (prop == 'overrideNewTab') { config.overrideNewTab = msg.data.overrideNewTab as boolean; } } dirtyMetadata = true; port.postMessage(msg.genResp(null)); break; } case MsgType.getConfig: { let res: {[prop: string]: any} = {}; msg.data.forEach((prop: string) => { if (prop === 'trackedPeriods') res.trackedPeriods = config.trackedPeriods.map(p => p.deflate()); else if (prop === 'overrideNewTab') res.overrideNewTab = config.overrideNewTab; }); port.postMessage(msg.genResp(res)); break; } case MsgType.getGraphData: { (async () => { await (msg.data.sync ? updateMainGraphData().then(() => {}) : Promise.resolve()); if (mainGraphData.length === 0) { await loadPromise; await updateMainGraphData(); } port.postMessage(msg.genResp(mainGraphData.map(d => ({ name: d.name, start: d.start.toISOString(), end: d.end.toISOString(), data: d.data })))); })(); break; } case MsgType.clearCache: { calData = {}; port.postMessage(msg.genResp(null)); break; } case MsgType.fetchCalendars: { (async () => { let token = await auth.getAuthToken(); let results = await gapi.getCalendars(token); port.postMessage(msg.genResp(results)); })(); break; } case MsgType.fetchColors: { (async () => { let token = await auth.getAuthToken(); let results = await gapi.getColors(token); port.postMessage(msg.genResp(results)); })(); break; } case MsgType.login: { (async () => { let succ = true; try { await auth.login(); } catch (_) { succ = false; } port.postMessage(msg.genResp(succ)); })(); break; } case MsgType.logout: { (async () => { let succ = true; try { await auth.logout(); } catch (_) { succ = false; } port.postMessage(msg.genResp(succ)); })(); break; } case MsgType.getLoggedIn: { auth.loggedIn().then(b => port.postMessage(msg.genResp(b))); break; } default: console.error("unknown msg opt"); } }); } loadPromise = (async () => { await Promise.all([loadMetadata(), loadCachedCals()]); pollSync(); })(); chrome.runtime.onConnect.addListener(handleMsg); chrome.tabs.onCreated.addListener(function(tab) { if (tab.url === "chrome://newtab/") { if (config.overrideNewTab) { chrome.tabs.update(tab.id, { url: chrome.extension.getURL("tab.html") }); } } }); chrome.runtime.onInstalled.addListener(async () => { try { await auth.logout(); } catch (_) {} });