cryptpad/www/common/outer/calendar.js

1211 lines
45 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

define([
'/common/common-util.js',
'/common/common-hash.js',
'/common/common-constants.js',
'/common/common-realtime.js',
'/common/outer/cache-store.js',
'/calendar/recurrence.js',
'/customize/messages.js',
'/bower_components/nthen/index.js',
'chainpad-listmap',
'/bower_components/chainpad-crypto/crypto.js',
'/bower_components/chainpad/chainpad.dist.js',
], function (Util, Hash, Constants, Realtime, Cache, Rec, 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 getRecurring = function (ev) {
var mid = new Date();
var start = new Date(mid.getFullYear(), mid.getMonth()-1, 15);
var end = new Date(mid.getFullYear(), mid.getMonth()+1, 15);
var startId = Rec.getMonthId(start);
var midId = Rec.getMonthId(mid);
var endId = Rec.getMonthId(end);
var toAdd = Rec.getRecurring([startId, midId, endId], [ev]);
var all = [ev];
Array.prototype.push.apply(all, toAdd);
return Rec.applyUpdates(all);
};
var clearDismissed = function (ctx, uid) {
var h = Util.find(ctx, ['store', 'proxy', 'hideReminders']) || {};
Object.keys(h).filter(function (id) {
return id.indexOf(uid) === 0;
}).forEach(function (id) {
delete h[id];
});
};
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] = [];
if (_ev.deleted) { return; }
var d = Util.find(ctx, ['store', 'proxy', 'hideReminders', uid]) || []; // dismissed
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];
clearDismissed(ctx, uid);
return;
}
var send = function (d) {
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+'-'+d
}, null, function () {
});
};
var sent = false;
var sendNotif = function (delay) {
sent = true;
ctx.Store.onReadyEvt.reg(function () {
send(delay);
});
};
var notifs = ev.reminders || [];
notifs.sort(function (a, b) {
return a - b;
});
notifs.some(function (delayMinutes) {
var delay = delayMinutes * 60000;
var time = now + delay;
if (d.some(function (minutes) {
return delayMinutes >= minutes;
})) { return; }
// 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(delayMinutes);
return true;
}
// It starts in more than "delay": prepare the notification
reminders[uid].push(setTimeout(function () {
sendNotif(delayMinutes);
}, (ev.start - time)));
});
if (!sent) {
// Remone any existing notification from the UI
ctx.Store.onReadyEvt.reg(function () {
ctx.store.mailbox.hideMessage('reminders', {
hash: 'REMINDER|'+uid
}, null, function () {
});
});
}
};
var updateEventReminders = function (ctx, reminders, ev, useLastVisit) {
var all = getRecurring(Util.clone(ev));
all.forEach(function (_ev) {
_updateEventReminders(ctx, reminders, _ev, useLastVisit);
});
};
var addReminders = function (ctx, id, ev) {
var calendar = ctx.calendars[id];
if (!ev) { return; } // XXX deleted event remote: delete reminders
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;
var _updateCalled = false;
var _update = function () {
if (_updateCalled) { return; }
_updateCalled = true;
setTimeout(function () {
_updateCalled = false;
update();
});
};
lm.proxy.on('cacheready', function () {
if (!proxy.metadata) { return; }
c.cacheready = true;
_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
};
}
_update();
if (cb) { cb(null, lm.proxy); }
addInitialReminders(ctx, channel, cfg.lastVisitNotif);
}).on('change', [], function () {
if (!c.ready) { return; }
_update();
}).on('change', ['content'], function (o, n, p) {
if (p.length === 2 && n && !o) { // New event
return void addReminders(ctx, channel, n);
}
if (p.length === 2 && !n && o) { // Deleted event
return void addReminders(ctx, channel, {
id: p[1],
start: 0
});
}
if (p.length >= 3 && ['start','reminders','isAllDay'].includes(p[2])) {
// Updated event
return void setTimeout(function () {
addReminders(ctx, channel, proxy.content[p[1]]);
});
}
if (p.length >= 6 && ['start','reminders','isAllDay'].includes(p[5])) {
// Updated recurring event
return void setTimeout(function () {
addReminders(ctx, channel, proxy.content[p[1]]);
});
}
}).on('remove', ['content'], function (x, p) {
_update();
if ((p.length >= 3 && p[2] === 'reminders') ||
(p.length >= 6 && p[5] === 'reminders')) {
return void 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;
_update();
}).on('reconnect', function () {
c.offline = false;
_update();
}).on('error', function (info) {
if (!info || !info.error) { return; }
if (info.error === "EDELETED" ) {
return void onDeleted();
}
if (info.error === "ERESTRICTED" ) {
c.restricted = true;
_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"}); }
data.rawData = data.rawData || {};
// update the event
var changes = data.changes || {};
var type = data.type || {};
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 || {};
}
var RECUPDATE = {
one: {},
from: {}
};
if (['one','from','all'].includes(type.which)) {
ev.recUpdate = ev.recUpdate || RECUPDATE;
if (!ev.recUpdate.one) { ev.recUpdate.one = {}; }
if (!ev.recUpdate.from) { ev.recUpdate.from = {}; }
}
var update = ev.recUpdate;
var alwaysAll = ['calendarId'];
var keys = Object.keys(changes).filter(function (s) {
// we can only change the calendar or recurrence rule on the origin
return !alwaysAll.includes(s);
});
// Delete (future) affected keys
var cleanAfter = function (time) {
[update.from, update.one].forEach(function (obj) {
Object.keys(obj).forEach(function (d) {
if (Number(d) < time) { return; }
delete obj[d];
});
});
};
var cleanKeys = function (obj, when) {
Object.keys(obj).forEach(function (d) {
if (when && Number(d) < when) { return; }
keys.forEach(function (k) {
delete obj[d][k];
});
});
};
// Update recurrence rule. We may create a new event here
var dontSendUpdate = false;
if (typeof(changes.recurrenceRule) !== "undefined") {
if (['one','from'].includes(type.which) && !data.rawData.isOrigin) {
cleanAfter(type.when);
} else {
update = ev.recUpdate = RECUPDATE;
}
}
if (type.which === "one") {
update.one[type.when] = update.one[type.when] || {};
// Nothing to delete
} else if (type.which === "from") {
update.from[type.when] = update.from[type.when] || {};
// Delete all "single/from" updates (affected keys only) after this "from" date
cleanKeys(update.from, type.when);
cleanKeys(update.one, type.when);
} else if (type.which === "all") {
// Delete all "single/from" updates (affected keys only) after
cleanKeys(update.from);
cleanKeys(update.one);
}
if (changes.start && (!type.which || type.which === "all")) {
var diff = changes.start - ev.start;
var newOne = {};
var newFrom = {};
Object.keys(update.one).forEach(function (time) {
newOne[Number(time)+diff] = update.one[time];
});
Object.keys(update.from).forEach(function (time) {
newFrom[Number(time)+diff] = update.from[time];
});
update.one = newOne;
update.from = newFrom;
}
// Clear the "dismissed" reminders when the user is updating reminders
var h = Util.find(ctx, ['store', 'proxy', 'hideReminders']) || {};
if (changes.reminders) {
if (type.which === 'one') {
if (!type.when || type.when === ev.start) { delete h[data.ev.id]; }
else { delete h[data.ev.id +'|'+ type.when]; }
} else if (type.which === "from") {
Object.keys(h).filter(function (id) {
return id.indexOf(data.ev.id) === 0;
}).forEach(function (id) {
var time = Number(id.split('|')[1]);
if (!time) { return; }
if (time < type.when) { return; }
delete h[id];
});
} else {
Object.keys(h).filter(function (id) {
return id.indexOf(data.ev.id) === 0;
}).forEach(function (id) {
delete h[id];
});
}
}
// Apply the changes
Object.keys(changes).forEach(function (key) {
if (!alwaysAll.includes(key) && type.which === "one") {
if (key === "recurrenceRule") {
if (data.rawData && data.rawData.isOrigin) {
return (ev[key] = changes[key]);
}
// Always "from", never "one" for recurrence rules
update.from[type.when] = update.from[type.when] || {};
return (update.from[type.when][key] = changes[key]);
}
update.one[type.when][key] = changes[key];
return;
}
if (!alwaysAll.includes(key) && type.which === "from") {
update.from[type.when][key] = changes[key];
return;
}
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);
}
if (!dontSendUpdate || newC) { sendUpdate(ctx, c); }
if (newC && !dontSendUpdate) { 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 || {};
var evId = data.id.split('|')[0];
if (data.id === evId) {
delete c.proxy.content[data.id];
} else {
var ev = c.proxy.content[evId];
var s = data.raw && data.raw.start;
if (s) {
ev.recUpdate = ev.recUpdate || {
one: {},
from: {}
};
ev.recUpdate.one[s] = {
deleted: true
};
}
}
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);
}));
ctx.store.proxy.on('change', ['hideReminders'], function (o,n,p) {
var uid = p[1].split('|')[0];
Object.keys(ctx.calendars).some(function (calId) {
var c = ctx.calendars[calId];
if (!c || !c.proxy || !c.proxy.content) { return; }
if (c.proxy.content[uid]) {
setTimeout(function () {
addReminders(ctx, calId, c.proxy.content[uid]);
});
return true;
}
});
});
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;
});