cryptpad/www/common/outer/calendar.js

982 lines
36 KiB
JavaScript

define([
'/common/common-util.js',
'/common/common-hash.js',
'/common/common-constants.js',
'/common/common-realtime.js',
'/common/outer/cache-store.js',
'/customize/messages.js',
'/bower_components/nthen/index.js',
'/bower_components/chainpad-listmap/chainpad-listmap.js',
'/bower_components/chainpad-crypto/crypto.js',
'/bower_components/chainpad/chainpad.dist.js',
], function (Util, Hash, Constants, Realtime, Cache, Messages, nThen, Listmap, Crypto, ChainPad) {
var Calendar = {};
var getStore = function (ctx, id) {
if (!id || id === 1) {
return ctx.store;
}
var m = ctx.store.modules && ctx.store.modules.team;
if (!m) { return; }
return m.getTeam(id);
};
var makeCalendar = function () {
var hash = Hash.createRandomHash('calendar');
var secret = Hash.getSecrets('calendar', hash);
var roHash = Hash.getViewHashFromKeys(secret);
var href = Hash.hashToHref(hash, 'calendar');
var roHref = Hash.hashToHref(roHash, 'calendar');
return {
href: href,
roHref: roHref,
channel: secret.channel,
};
};
var initializeCalendars = function (ctx, cb) {
var proxy = ctx.store.proxy;
proxy.calendars = proxy.calendars || {};
setTimeout(cb);
};
var sendUpdate = function (ctx, c) {
ctx.emit('UPDATE', {
teams: c.stores,
roTeams: c.roStores,
id: c.channel,
loading: !c.ready && !c.cacheready,
readOnly: c.readOnly || (!c.ready && c.cacheready) || c.offline,
offline: c.offline,
deleted: !c.stores.length,
restricted: c.restricted,
owned: ctx.Store.isOwned(c.owners),
content: Util.clone(c.proxy),
hashes: c.hashes
}, ctx.clients);
};
var clearReminders = function (ctx, id) {
var calendar = ctx.calendars[id];
if (!calendar || !calendar.reminders) { return; }
// Clear existing reminders
Object.keys(calendar.reminders).forEach(function (uid) {
if (!Array.isArray(calendar.reminders[uid])) { return; }
calendar.reminders[uid].forEach(function (to) { clearTimeout(to); });
});
};
var closeCalendar = function (ctx, id) {
var ctxCal = ctx.calendars[id];
if (!ctxCal) { return; }
// If the calendar doesn't exist in any other team, stop it and delete it from ctx
if (!ctxCal.stores.length) {
ctxCal.lm.stop();
clearReminders(ctx, id);
delete ctx.calendars[id];
}
};
var updateLocalCalendars = function (ctx, c, data) {
// Also update local data
c.stores.forEach(function (id) {
var s = getStore(ctx, id);
if (!s || !s.proxy) { return; }
if (!s.rpc) { return; } // team viewer
if (!s.proxy.calendars) { return; }
var cal = s.proxy.calendars[c.channel];
if (!cal) { return; }
if (cal.color !== data.color) { cal.color = data.color; }
if (cal.title !== data.title) { cal.title = data.title; }
});
};
var updateEventReminders = function (ctx, reminders, _ev, useLastVisit) {
var now = +new Date();
var ev = Util.clone(_ev);
var uid = ev.id;
// Clear reminders for this event
if (Array.isArray(reminders[uid])) {
reminders[uid].forEach(function (to) { clearTimeout(to); });
}
reminders[uid] = [];
var last = ctx.store.data.lastVisit;
if (ev.isAllDay) {
if (ev.startDay) { ev.start = +new Date(ev.startDay); }
if (ev.endDay) {
var endDate = new Date(ev.endDay);
endDate.setHours(23);
endDate.setMinutes(59);
endDate.setSeconds(59);
ev.end = +endDate;
}
}
var oneWeekAgo = now - (7 * 24 * 3600 * 1000);
var missed = useLastVisit && ev.start > last && ev.end <= now && ev.end > oneWeekAgo;
if (ev.end <= now && !missed) {
// No reminder for past events
delete reminders[uid];
return;
}
var send = function () {
var hide = Util.find(ctx, ['store', 'proxy', 'settings', 'general', 'calendar', 'hideNotif']);
if (hide) { return; }
var ctime = ev.start <= now ? ev.start : +new Date(); // Correct order for past events
ctx.store.mailbox.showMessage('reminders', {
msg: {
ctime: ctime,
type: "REMINDER",
missed: Boolean(missed),
content: ev
},
hash: 'REMINDER|'+uid
}, null, function () {
});
};
var sendNotif = function () { ctx.Store.onReadyEvt.reg(send); };
var notifs = ev.reminders || [];
notifs.sort(function (a, b) {
return a - b;
});
notifs.some(function (delayMinutes) {
var delay = delayMinutes * 60000;
var time = now + delay;
// setTimeout only work with 32bit timeout values. If the event is too far away,
// ignore this event for now
// FIXME: call this function again in xxx days to reload these missing timeout?
if (ev.start - time >= 2147483647) { return true; }
// If we're too late to send a notification, send it instantly and ignore
// all notifications that were supposed to be sent even earlier
if (ev.start <= time) {
sendNotif();
return true;
}
// It starts in more than "delay": prepare the notification
reminders[uid].push(setTimeout(function () {
sendNotif();
}, (ev.start - time)));
});
};
var addReminders = function (ctx, id, ev) {
var calendar = ctx.calendars[id];
if (!calendar || !calendar.reminders) { return; }
if (calendar.stores.length === 1 && calendar.stores[0] === 0) { return; }
updateEventReminders(ctx, calendar.reminders, ev);
};
var addInitialReminders = function (ctx, id, useLastVisit) {
var calendar = ctx.calendars[id];
if (!calendar || !calendar.reminders) { return; }
if (Object.keys(calendar.reminders).length) { return; } // Already initialized
// No reminders for calendars not stored
if (calendar.stores.length === 1 && calendar.stores[0] === 0) { return; }
// Re-add all reminders
var content = Util.find(calendar, ['proxy', 'content']);
if (!content) { return; }
Object.keys(content).forEach(function (uid) {
updateEventReminders(ctx, calendar.reminders, content[uid], useLastVisit);
});
};
var openChannel = function (ctx, cfg, _cb) {
var cb = Util.once(Util.mkAsync(_cb || function () {}));
var teamId = cfg.storeId;
var data = cfg.data;
var channel = data.channel;
if (!channel) { return; }
var c = ctx.calendars[channel];
var update = function () {
sendUpdate(ctx, c);
};
if (c) {
if (c.readOnly && data.href) {
// Upgrade readOnly calendar to editable
var upgradeParsed = Hash.parsePadUrl(data.href);
var upgradeSecret = Hash.getSecrets('calendar', upgradeParsed.hash, data.password);
var upgradeCrypto = Crypto.createEncryptor(upgradeSecret.keys);
c.hashes.editHash = Hash.getEditHashFromKeys(upgradeSecret);
c.lm.setReadOnly(false, upgradeCrypto);
c.readOnly = false;
} else if (teamId === 0) {
// If we open a second tab with the same temp URL, push to tempId
if (c.stores.length === 1 && c.stores[0] === 0 && c.tempId.length && cfg.cId) {
c.tempId.push(cfg.cId);
}
// Existing calendars can't be "temp calendars" (unless they are an upgrade)
return void cb();
}
// Remove from roStores when upgrading this store
if (c.roStores.indexOf(teamId) !== -1 && data.href) {
c.roStores.splice(c.roStores.indexOf(teamId), 1);
// If we've upgraded a stored calendar, remove the temp calendar
if (c.stores.indexOf(0) !== -1) {
c.stores.splice(c.stores.indexOf(0), 1);
}
update();
}
// Don't store duplicates
if (c.stores && c.stores.indexOf(teamId) !== -1) { return void cb(); }
// If we store a temp calendar to our account or team, remove this "temp calendar"
if (c.stores.indexOf(0) !== -1) {
c.stores.splice(c.stores.indexOf(0), 1);
c.tempId = [];
}
c.stores.push(teamId);
if (!data.href) {
c.roStores.push(teamId);
}
update();
return void cb();
}
// Multiple teams can have the same calendar. Make sure we remember the list of stores
// that know it so that we don't close the calendar when leaving/deleting a team.
c = ctx.calendars[channel] = {
ready: false,
channel: channel,
readOnly: !data.href,
tempId: [],
stores: [teamId],
roStores: data.href ? [] : [teamId],
reminders: {},
hashes: {}
};
if (teamId === 0) {
c.tempId.push(cfg.cId);
}
var parsed = Hash.parsePadUrl(data.href || data.roHref);
var secret = Hash.getSecrets('calendar', parsed.hash, data.password);
var crypto = Crypto.createEncryptor(secret.keys);
c.hashes.viewHash = Hash.getViewHashFromKeys(secret);
if (data.href) {
c.hashes.editHash = Hash.getEditHashFromKeys(secret);
}
c.proxy = {
metadata: {
color: data.color,
title: data.title
}
};
update();
var onDeleted = function () {
// Remove this calendar from all our teams
c.stores.forEach(function (storeId) {
var store = getStore(ctx, storeId);
if (!store || !store.rpc || !store.proxy.calendars) { return; }
delete store.proxy.calendars[channel];
// And unpin
var unpin = store.unpin || ctx.unpinPads;
unpin([channel], function (res) {
if (res && res.error) { console.error(res.error); }
});
});
// Close listmap, update the UI and clear the memory
if (c.lm) { c.lm.stop(); }
c.stores = [];
sendUpdate(ctx, c);
clearReminders(ctx, channel);
delete ctx.calendars[channel];
};
nThen(function (waitFor) {
if (!ctx.store.network || cfg.isNew) { return; }
// This is supposed to be an existing channel. Make sure it exists on the server
// before trying to load it.
// NOTE: if we can't check (error), we can skip this step. On "ready", we have
// another check to make sure we won't make a new calendar
ctx.Store.isNewChannel(null, channel, waitFor(function (obj) {
if (obj && obj.error) {
// If we can't check, skip this part
return;
}
if (obj && typeof(obj.isNew) === "boolean") {
if (obj.isNew) {
onDeleted();
cb({error: 'EDELETED'});
waitFor.abort();
return;
}
}
}));
}).nThen(function () {
// Set the owners as the first store opening it. We don't know yet if it's a new or
// existing calendar. "owners' will be ignored if the calendar already exists.
var edPublic;
if (teamId === 1 || !teamId) {
edPublic = ctx.store.proxy.edPublic;
} else {
var teams = ctx.store.modules.team && ctx.store.modules.team.getTeamsData();
var team = teams && teams[teamId];
edPublic = team ? team.edPublic : undefined;
}
var config = {
data: {},
network: ctx.store.network || ctx.store.networkPromise,
channel: secret.channel,
crypto: crypto,
owners: [edPublic],
ChainPad: ChainPad,
validateKey: secret.keys.validateKey || undefined,
userName: 'calendar',
Cache: Cache,
classic: true,
onRejected: ctx.Store && ctx.Store.onRejected
};
var lm = Listmap.create(config);
c.lm = lm;
var proxy = c.proxy = lm.proxy;
lm.proxy.on('cacheready', function () {
if (!proxy.metadata) { return; }
c.cacheready = true;
setTimeout(update);
if (cb) { cb(null, lm.proxy); }
addInitialReminders(ctx, channel, cfg.lastVisitNotif);
}).on('ready', function (info) {
var md = info.metadata;
c.owners = md.owners || [];
c.ready = true;
if (!proxy.metadata) {
if (!cfg.isNew) {
// no metadata on an existing calendar: deleted calendar
return void onDeleted();
}
proxy.metadata = {
color: data.color,
title: data.title
};
}
setTimeout(update);
if (cb) { cb(null, lm.proxy); }
addInitialReminders(ctx, channel, cfg.lastVisitNotif);
}).on('change', [], function () {
if (!c.ready) { return; }
setTimeout(update);
}).on('change', ['content'], function (o, n, p) {
if (p.length === 2 && n && !o) { // New event
addReminders(ctx, channel, n);
}
if (p.length === 2 && !n && o) { // Deleted event
addReminders(ctx, channel, {
id: p[1],
start: 0
});
}
if (p.length === 3 && n && o && p[2] === 'start') { // Update event start
setTimeout(function () {
addReminders(ctx, channel, proxy.content[p[1]]);
});
}
}).on('change', ['metadata'], function () {
// if title or color have changed, update our local values
var md = proxy.metadata;
if (!md || !md.title || !md.color) { return; }
updateLocalCalendars(ctx, c, md);
}).on('disconnect', function () {
c.offline = true;
setTimeout(update);
}).on('reconnect', function () {
c.offline = false;
setTimeout(update);
}).on('error', function (info) {
if (!info || !info.error) { return; }
if (info.error === "EDELETED" ) {
return void onDeleted();
}
if (info.error === "ERESTRICTED" ) {
c.restricted = true;
setTimeout(update);
}
cb(info);
});
});
};
var decryptTeamCalendarHref = function (store, calData) {
if (!calData.href) { return; }
// Already decrypted? nothing to do
if (calData.href.indexOf('#') !== -1) { return; }
// href exists and is encrypted: decrypt if we can or ignore the href
if (store.secondaryKey) {
try {
calData.href = store.userObject.cryptor.decrypt(calData.href);
} catch (e) {
console.error(e);
delete calData.href;
}
} else {
delete calData.href;
}
};
var initializeStore = function (ctx, store) {
var c = store.proxy.calendars;
var storeId = store.id || 1;
// Add listeners
store.proxy.on('change', ['calendars'], function (o, n, p) {
if (p.length < 2) { return; }
// Handle deletions
if (o && !n) {
(function () {
var id = p[1];
var ctxCal = ctx.calendars[id];
if (!ctxCal) { return; }
var idx = ctxCal.stores.indexOf(storeId);
// Check if the team has loaded this calendar in memory
if (idx === -1) { return; }
// Remove the team from memory
ctxCal.stores.splice(idx, 1);
var roIdx = ctxCal.roStores.indexOf(storeId);
if (roIdx !== -1) { ctxCal.roStores.splice(roIdx, 1); }
// Check if we need to close listmap and update the UI
closeCalendar(ctx, id);
sendUpdate(ctx, ctxCal);
})();
}
// Handle additions
// NOTE: this also upgrade from readOnly to edit (add an "href" to the calendar)
if (!o && n) {
(function () {
var id = p[1];
var _cal = store.proxy.calendars[id];
if (!_cal) { return; }
var cal = Util.clone(_cal);
decryptTeamCalendarHref(store, cal);
openChannel(ctx, {
storeId: storeId,
data: cal
});
})();
}
});
// If this store contains existing calendars, open them
Object.keys(c || {}).forEach(function (channel) {
var cal = Util.clone(c[channel]);
decryptTeamCalendarHref(store, cal);
openChannel(ctx, {
storeId: storeId,
lastVisitNotif: true,
data: cal
});
});
};
var openChannels = function (ctx) {
// Personal drive
initializeStore(ctx, ctx.store);
var teams = ctx.store.modules.team && ctx.store.modules.team.getTeamsData();
if (!teams) { return; }
Object.keys(teams).forEach(function (id) {
var store = getStore(ctx, id);
initializeStore(ctx, store);
});
};
var subscribe = function (ctx, data, cId, cb) {
// Subscribe to new notifications
var idx = ctx.clients.indexOf(cId);
if (idx === -1) {
ctx.clients.push(cId);
}
cb({
empty: !Object.keys(ctx.calendars).length
});
Object.keys(ctx.calendars).forEach(function (channel) {
var c = ctx.calendars[channel] || {};
sendUpdate(ctx, c);
});
};
var importICSCalendar = function (ctx, data, cId, cb) {
var id = data.id;
var c = ctx.calendars[id];
if (!c || !c.proxy) { return void cb({error: "ENOENT"}); }
var json = data.json;
c.proxy.content = c.proxy.content || {};
Object.keys(json).forEach(function (uid) {
c.proxy.content[uid] = json[uid];
addReminders(ctx, id, json[uid]);
});
Realtime.whenRealtimeSyncs(c.lm.realtime, function () {
sendUpdate(ctx, c);
cb();
});
};
var openCalendar = function (ctx, data, cId, cb) {
var secret = Hash.getSecrets('calendar', data.hash, data.password);
var hash = Hash.getEditHashFromKeys(secret);
var roHash = Hash.getViewHashFromKeys(secret);
if (!ctx.loggedIn) { hash = undefined; }
var cal = {
href: hash && Hash.hashToHref(hash, 'calendar'),
roHref: roHash && Hash.hashToHref(roHash, 'calendar'),
channel: secret.channel,
color: Util.getRandomColor(),
title: '...'
};
openChannel(ctx, {
cId: cId,
storeId: 0,
data: cal
}, cb);
};
var importCalendar = function (ctx, data, cId, cb) {
var id = data.id;
var c = ctx.calendars[id];
if (!c) { return void cb({error: "ENOENT"}); }
if (!Array.isArray(c.stores) || c.stores.indexOf(data.teamId) === -1) {
return void cb({error: 'EINVAL'});
}
// Add to my calendars
var store = ctx.store;
var calendars = store.proxy.calendars = store.proxy.calendars || {};
var hash = c.hashes.editHash;
var roHash = c.hashes.viewHash;
calendars[id] = {
href: hash && Hash.hashToHref(hash, 'calendar'),
roHref: roHash && Hash.hashToHref(roHash, 'calendar'),
channel: id,
color: Util.find(c,['proxy', 'metadata', 'color']) || Util.getRandomColor(),
title: Util.find(c,['proxy', 'metadata', 'title']) || '...'
};
ctx.Store.onSync(null, cb);
// Make the change in memory
openChannel(ctx, {
storeId: 1,
data: {
href: calendars[id].href,
toHref: calendars[id].roHref,
channel: id
}
});
};
var addCalendar = function (ctx, data, cId, cb) {
var store = getStore(ctx, data.teamId);
if (!store) { return void cb({error: "NO_STORE"}); }
// Check team edit rights: viewers in teams don't have rpc
if (!store.rpc) { return void cb({error: "EFORBIDDEN"}); }
var c = store.proxy.calendars = store.proxy.calendars || {};
var parsed = Hash.parsePadUrl(data.href);
var secret = Hash.getSecrets(parsed.type, parsed.hash, data.password);
if (secret.channel !== data.channel) { return void cb({error: 'EINVAL'}); }
var hash = Hash.getEditHashFromKeys(secret);
var roHash = Hash.getViewHashFromKeys(secret);
var href = hash && Hash.hashToHref(hash, 'calendar');
var cal = {
href: href,
roHref: roHash && Hash.hashToHref(roHash, 'calendar'),
color: data.color,
title: data.title,
channel: data.channel
};
// If it already existed and it's not an upgrade, nothing to do
if (c[data.channel] && (c[data.channel].href || !cal.href)) { return void cb(); }
cal.color = data.color;
cal.title = data.title;
openChannel(ctx, {
storeId: store.id || 1,
data: Util.clone(cal)
}, function (err) {
if (err) {
// Can't open this channel, don't store it
console.error(err);
return void cb({error: err.error});
}
if (href && store.id && store.secondaryKey) {
try {
cal.href = store.userObject.cryptor.encrypt(href);
} catch (e) {
console.error(e);
}
}
// Add the calendar and call back
// If it already existed it means this is an upgrade
c[cal.channel] = cal;
var pin = store.pin || ctx.pinPads;
pin([cal.channel], function (res) {
if (res && res.error) { console.error(res.error); }
});
ctx.Store.onSync(store.id, cb);
});
};
var createCalendar = function (ctx, data, cId, cb) {
var store = getStore(ctx, data.teamId);
if (!store) { return void cb({error: "NO_STORE"}); }
// Check team edit rights: viewers in teams don't have rpc
if (!store.rpc) { return void cb({error: "EFORBIDDEN"}); }
var c = store.proxy.calendars = store.proxy.calendars || {};
var cal = makeCalendar();
cal.color = data.color;
cal.title = data.title;
openChannel(ctx, {
storeId: store.id || 1,
data: cal,
isNew: true
}, function (err) {
if (err) {
// Can't open this channel, don't store it
console.error(err);
return void cb({error: err.error});
}
// Add the calendar and call back
// Wait for the metadata to be stored (channel fully ready) before adding it
// to our store
var ctxCal = ctx.calendars[cal.channel];
Realtime.whenRealtimeSyncs(ctxCal.lm.realtime, function () {
c[cal.channel] = cal;
var pin = store.pin || ctx.pinPads;
pin([cal.channel], function (res) {
if (res && res.error) { console.error(res.error); }
});
ctx.Store.onSync(store.id, cb);
});
});
};
var updateCalendar = function (ctx, data, cId, cb) {
var id = data.id;
var c = ctx.calendars[id];
if (!c) { return void cb({error: "ENOENT"}); }
var md = Util.find(c, ['proxy', 'metadata']);
if (!md) { return void cb({error: 'EINVAL'}); }
md.title = data.title;
md.color = data.color;
Realtime.whenRealtimeSyncs(c.lm.realtime, cb);
sendUpdate(ctx, c);
updateLocalCalendars(ctx, c, data);
};
var deleteCalendar = function (ctx, data, cId, cb) {
var store = getStore(ctx, data.teamId);
if (!store) { return void cb({error: "NO_STORE"}); }
if (!store.rpc) { return void cb({error: "EFORBIDDEN"}); }
if (!store.proxy.calendars) { return; }
var id = data.id;
var cal = store.proxy.calendars[id];
if (!cal) { return void cb(); } // Already deleted
// Delete
delete store.proxy.calendars[id];
// Unpin
var unpin = store.unpin || ctx.unpinPads;
unpin([id], function (res) {
if (res && res.error) { console.error(res.error); }
});
// Clear/update ctx data
// Remove this store from the calendar's clients
var ctxCal = ctx.calendars[id];
var idx = ctxCal.stores.indexOf(store.id || 1);
ctxCal.stores.splice(idx, 1);
closeCalendar(ctx, id);
ctx.Store.onSync(store.id, function () {
sendUpdate(ctx, ctxCal);
cb();
});
};
var createEvent = function (ctx, data, cId, cb) {
var id = data.calendarId;
var c = ctx.calendars[id];
if (!c) { return void cb({error: "ENOENT"}); }
var startDate = new Date(data.start);
var endDate = new Date(data.end);
if (data.isAllDay) {
data.startDay = startDate.getFullYear() + '-' + (startDate.getMonth()+1) + '-' + startDate.getDate();
data.endDay = endDate.getFullYear() + '-' + (endDate.getMonth()+1) + '-' + endDate.getDate();
} else {
delete data.startDay;
delete data.endDay;
}
c.proxy.content = c.proxy.content || {};
c.proxy.content[data.id] = data;
Realtime.whenRealtimeSyncs(c.lm.realtime, function () {
addReminders(ctx, id, data);
sendUpdate(ctx, c);
cb();
});
};
var updateEvent = function (ctx, data, cId, cb) {
if (!data || !data.ev) { return void cb({error: 'EINVAL'}); }
var id = data.ev.calendarId;
var c = ctx.calendars[id];
if (!c || !c.proxy || !c.proxy.content) { return void cb({error: "ENOENT"}); }
// Find the event
var ev = c.proxy.content[data.ev.id];
if (!ev) { return void cb({error: "EINVAL"}); }
// update the event
var changes = data.changes || {};
var newC;
if (changes.calendarId) {
newC = ctx.calendars[changes.calendarId];
if (!newC || !newC.proxy) { return void cb({error: "ENOENT"}); }
newC.proxy.content = newC.proxy.content || {};
}
Object.keys(changes).forEach(function (key) {
ev[key] = changes[key];
});
var startDate = new Date(ev.start);
var endDate = new Date(ev.end);
if (ev.isAllDay) {
ev.startDay = startDate.getFullYear() + '-' + (startDate.getMonth()+1) + '-' + startDate.getDate();
ev.endDay = endDate.getFullYear() + '-' + (endDate.getMonth()+1) + '-' + endDate.getDate();
} else {
delete ev.startDay;
delete ev.endDay;
}
// Move to a different calendar?
if (changes.calendarId && newC) {
newC.proxy.content[data.ev.id] = Util.clone(ev);
delete c.proxy.content[data.ev.id];
}
nThen(function (waitFor) {
Realtime.whenRealtimeSyncs(c.lm.realtime, waitFor());
if (newC) { Realtime.whenRealtimeSyncs(newC.lm.realtime, waitFor()); }
}).nThen(function () {
if (newC) {
// Move reminders to the new calendar
addReminders(ctx, id, {
id: ev.id,
start: 0
});
addReminders(ctx, ev.calendarId, ev);
} else if (changes.start || changes.reminders || changes.isAllDay) {
// Update reminders
addReminders(ctx, id, ev);
}
sendUpdate(ctx, c);
if (newC) { sendUpdate(ctx, newC); }
cb();
});
};
var deleteEvent = function (ctx, data, cId, cb) {
var id = data.calendarId;
var c = ctx.calendars[id];
if (!c) { return void cb({error: "ENOENT"}); }
c.proxy.content = c.proxy.content || {};
delete c.proxy.content[data.id];
Realtime.whenRealtimeSyncs(c.lm.realtime, function () {
addReminders(ctx, id, {
id: data.id,
start: 0
});
sendUpdate(ctx, c);
cb();
});
};
var removeClient = function (ctx, cId) {
var idx = ctx.clients.indexOf(cId);
ctx.clients.splice(idx, 1);
Object.keys(ctx.calendars).forEach(function (id) {
var cal = ctx.calendars[id];
if (cal.stores.length !== 1 || cal.stores[0] !== 0 || !cal.tempId.length) { return; }
// This is a temp calendar: check if the closed tab had this calendar opened
var idx = cal.tempId.indexOf(cId);
if (idx !== -1) { cal.tempId.splice(idx, 1); }
if (!cal.tempId.length) {
cal.stores = [];
// Close calendar
closeCalendar(ctx, id);
}
});
};
Calendar.init = function (cfg, waitFor, emit) {
var calendar = {};
var store = cfg.store;
//if (!store.loggedIn || !store.proxy.edPublic) { return; } // XXX logged in only? we should al least allow read-only for URL calendars
var ctx = {
loggedIn: store.loggedIn && store.proxy.edPublic,
store: store,
Store: cfg.Store,
pinPads: cfg.pinPads,
unpinPads: cfg.unpinPads,
updateMetadata: cfg.updateMetadata,
emit: emit,
onReady: Util.mkEvent(true),
calendars: {},
clients: []
};
initializeCalendars(ctx, waitFor(function (err) {
if (err) { return; }
openChannels(ctx);
}));
calendar.closeTeam = function (teamId) {
Object.keys(ctx.calendars).forEach(function (id) {
var ctxCal = ctx.calendars[id];
var idx = ctxCal.stores.indexOf(teamId);
if (idx === -1) { return; }
ctxCal.stores.splice(idx, 1);
var roIdx = ctxCal.roStores.indexOf(teamId);
if (roIdx !== -1) { ctxCal.roStores.splice(roIdx, 1); }
closeCalendar(ctx, id);
sendUpdate(ctx, ctxCal);
});
};
calendar.openTeam = function (teamId) {
var store = getStore(ctx, teamId);
if (!store) { return; }
initializeStore(ctx, store);
};
calendar.upgradeTeam = function (teamId) {
if (!teamId) { return; }
var store = getStore(ctx, teamId);
if (!store) { return; }
Object.keys(ctx.calendars).forEach(function (id) {
var ctxCal = ctx.calendars[id];
var idx = ctxCal.stores.indexOf(teamId);
if (idx === -1) { return; }
var _cal = store.proxy.calendars[id];
var cal = Util.clone(_cal);
decryptTeamCalendarHref(store, cal);
openChannel(ctx, {
storeId: teamId,
data: cal
});
sendUpdate(ctx, ctxCal);
});
};
calendar.removeClient = function (clientId) {
removeClient(ctx, clientId);
};
calendar.execCommand = function (clientId, obj, cb) {
var cmd = obj.cmd;
var data = obj.data;
if (cmd === 'SUBSCRIBE') {
return void subscribe(ctx, data, clientId, cb);
}
if (cmd === 'OPEN') {
ctx.Store.onReadyEvt.reg(function () {
openCalendar(ctx, data, clientId, cb);
});
return;
}
if (cmd === 'IMPORT') {
if (ctx.store.offline) { return void cb({error: 'OFFLINE'}); }
if (!ctx.loggedIn) { return void cb({error: 'NOT_LOGGED_IN'}); }
return void importCalendar(ctx, data, clientId, cb);
}
if (cmd === 'IMPORT_ICS') {
if (ctx.store.offline) { return void cb({error: 'OFFLINE'}); }
if (!ctx.loggedIn) { return void cb({error: 'NOT_LOGGED_IN'}); }
return void importICSCalendar(ctx, data, clientId, cb);
}
if (cmd === 'ADD') {
if (ctx.store.offline) { return void cb({error: 'OFFLINE'}); }
if (!ctx.loggedIn) { return void cb({error: 'NOT_LOGGED_IN'}); }
return void addCalendar(ctx, data, clientId, cb);
}
if (cmd === 'CREATE') {
if (!ctx.loggedIn) { return void cb({error: 'NOT_LOGGED_IN'}); }
if (data.initialCalendar) {
return void ctx.Store.onReadyEvt.reg(function () {
createCalendar(ctx, data, clientId, cb);
});
}
if (ctx.store.offline) { return void cb({error: 'OFFLINE'}); }
return void createCalendar(ctx, data, clientId, cb);
}
if (cmd === 'UPDATE') {
if (ctx.store.offline) { return void cb({error: 'OFFLINE'}); }
if (!ctx.loggedIn) { return void cb({error: 'NOT_LOGGED_IN'}); }
return void updateCalendar(ctx, data, clientId, cb);
}
if (cmd === 'DELETE') {
if (ctx.store.offline) { return void cb({error: 'OFFLINE'}); }
if (!ctx.loggedIn) { return void cb({error: 'NOT_LOGGED_IN'}); }
return void deleteCalendar(ctx, data, clientId, cb);
}
if (cmd === 'CREATE_EVENT') {
if (ctx.store.offline) { return void cb({error: 'OFFLINE'}); }
if (!ctx.loggedIn) { return void cb({error: 'NOT_LOGGED_IN'}); }
return void createEvent(ctx, data, clientId, cb);
}
if (cmd === 'UPDATE_EVENT') {
if (ctx.store.offline) { return void cb({error: 'OFFLINE'}); }
if (!ctx.loggedIn) { return void cb({error: 'NOT_LOGGED_IN'}); }
return void updateEvent(ctx, data, clientId, cb);
}
if (cmd === 'DELETE_EVENT') {
if (ctx.store.offline) { return void cb({error: 'OFFLINE'}); }
if (!ctx.loggedIn) { return void cb({error: 'NOT_LOGGED_IN'}); }
return void deleteEvent(ctx, data, clientId, cb);
}
};
return calendar;
};
return Calendar;
});