From 48c3786eb0d68e223464573636c46441c2ee3f89 Mon Sep 17 00:00:00 2001 From: Bertrand d'Aure Date: Fri, 13 Sep 2024 14:49:29 +0200 Subject: [PATCH] Caldav integration: Handle new until date for recurring events (#2107) --- server/lib/calendar/calendar.destroyEvents.js | 28 ++++++++++++---- .../calendar/calendar.syncUserCalendars.js | 14 ++++++++ server/services/caldav/package-lock.json | 33 +++++++++++++++++++ server/services/caldav/package.json | 1 + .../test/lib/calendar/calendar.event.test.js | 24 +++++++++++++- .../lib/calendar/syncUserCalendars.test.js | 16 +++++++++ 6 files changed, 108 insertions(+), 8 deletions(-) diff --git a/server/lib/calendar/calendar.destroyEvents.js b/server/lib/calendar/calendar.destroyEvents.js index f2acb6067e..532beb7202 100644 --- a/server/lib/calendar/calendar.destroyEvents.js +++ b/server/lib/calendar/calendar.destroyEvents.js @@ -1,17 +1,31 @@ +const { Op } = require('sequelize'); const db = require('../../models'); /** * @description Delete events from a calendar. * @param {string} calendarId - Calendar id to empty. + * @param {object} options - Options of the query. * @example - * gladys.calendar.destroyEvents('0dc03aef-4a23-9c4e-88e3-5437971269e5'); + * gladys.calendar.destroyEvents('0dc03aef-4a23-9c4e-88e3-5437971269e5', {url: '/calendar/event.ics'}); */ -async function destroyEvents(calendarId) { - await db.CalendarEvent.destroy({ - where: { - calendar_id: calendarId, - }, - }); +async function destroyEvents(calendarId, options = {}) { + const where = { + calendar_id: calendarId, + }; + + if (options.url) { + where.url = { + [Op.eq]: options.url, + }; + } + + if (options.from) { + where.start = { + [Op.gte]: new Date(options.from), + }; + } + + await db.CalendarEvent.destroy({ where }); } module.exports = { diff --git a/server/services/caldav/lib/calendar/calendar.syncUserCalendars.js b/server/services/caldav/lib/calendar/calendar.syncUserCalendars.js index 5b71c98edd..b38b83542e 100644 --- a/server/services/caldav/lib/calendar/calendar.syncUserCalendars.js +++ b/server/services/caldav/lib/calendar/calendar.syncUserCalendars.js @@ -1,4 +1,5 @@ const Promise = require('bluebird'); +const get = require('get-value'); const logger = require('../../../../utils/logger'); const { ServiceNotConfiguredError, NotFoundError } = require('../../../../utils/coreErrors'); @@ -108,6 +109,19 @@ async function syncUserCalendars(userId) { throw new NotFoundError('CALDAV_FAILED_REQUEST_EVENTS'); } + await Promise.map( + jsonEvents, + async (jsonEvent) => { + if (get(jsonEvent, 'rrule.options.until') && jsonEvent.href) { + await this.gladys.calendar.destroyEvents(calendarToUpdate.id, { + url: jsonEvent.href, + from: get(jsonEvent, 'rrule.options.until'), + }); + } + }, + { concurrency: 5 }, + ); + const formatedEvents = this.formatEvents(jsonEvents, calendarToUpdate); let insertedOrUpdatedEvent = 0; diff --git a/server/services/caldav/package-lock.json b/server/services/caldav/package-lock.json index 5ff5378e92..11532d5f57 100644 --- a/server/services/caldav/package-lock.json +++ b/server/services/caldav/package-lock.json @@ -19,6 +19,7 @@ "bluebird": "^3.7.0", "dav-request": "^1.8.0", "dayjs": "^1.11.10", + "get-value": "^3.0.1", "ical": "^0.8.0", "xmldom": "^0.6.0" } @@ -79,6 +80,17 @@ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" }, + "node_modules/get-value": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-3.0.1.tgz", + "integrity": "sha512-mKZj9JLQrwMBtj5wxi6MH8Z5eSKaERpAwjg43dPtlGI1ZVEgH/qC7T8/6R2OBSUA+zzHBZgICsVJaEIV2tKTDA==", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=6.0" + } + }, "node_modules/ical": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/ical/-/ical-0.8.0.tgz", @@ -87,6 +99,14 @@ "rrule": "2.4.1" } }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/luxon": { "version": "1.28.0", "resolved": "https://registry.npmjs.org/luxon/-/luxon-1.28.0.tgz", @@ -174,6 +194,14 @@ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" }, + "get-value": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-3.0.1.tgz", + "integrity": "sha512-mKZj9JLQrwMBtj5wxi6MH8Z5eSKaERpAwjg43dPtlGI1ZVEgH/qC7T8/6R2OBSUA+zzHBZgICsVJaEIV2tKTDA==", + "requires": { + "isobject": "^3.0.1" + } + }, "ical": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/ical/-/ical-0.8.0.tgz", @@ -182,6 +210,11 @@ "rrule": "2.4.1" } }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==" + }, "luxon": { "version": "1.28.0", "resolved": "https://registry.npmjs.org/luxon/-/luxon-1.28.0.tgz", diff --git a/server/services/caldav/package.json b/server/services/caldav/package.json index b6d1448a35..793a6a7794 100644 --- a/server/services/caldav/package.json +++ b/server/services/caldav/package.json @@ -16,6 +16,7 @@ "bluebird": "^3.7.0", "dav-request": "^1.8.0", "dayjs": "^1.11.10", + "get-value": "^3.0.1", "ical": "^0.8.0", "xmldom": "^0.6.0" } diff --git a/server/test/lib/calendar/calendar.event.test.js b/server/test/lib/calendar/calendar.event.test.js index eff161ce85..a4d1bc40a3 100644 --- a/server/test/lib/calendar/calendar.event.test.js +++ b/server/test/lib/calendar/calendar.event.test.js @@ -52,8 +52,8 @@ describe('calendar.destroy', () => { }); describe('calendar.destroyEvents', () => { - const calendar = new Calendar(); it("should destroy all calendar's event", async () => { + const calendar = new Calendar(); await calendar.destroyEvents('07ec2599-3221-4d6c-ac56-41443973201b'); const allCalendarEvents = await calendar.getEvents( '0cd30aef-9c4e-4a23-88e3-3547971296e5', @@ -61,6 +61,28 @@ describe('calendar.destroyEvents', () => { ); assert.deepEqual(allCalendarEvents, []); }); + + it("should destroy calendar's events by url", async () => { + const calendar = new Calendar(); + await calendar.destroyEvents('07ec2599-3221-4d6c-ac56-41443973201b', { + url: '/remote.php/dav/calendars/tony/personal/eee42d70-24f2-4c18-949d-822f3f72594c.ics', + }); + const allCalendarEvents = await calendar.getEvents( + '0cd30aef-9c4e-4a23-88e3-3547971296e5', + '07ec2599-3221-4d6c-ac56-41443973201b', + ); + expect(allCalendarEvents.length).eq(1); + }); + + it("should destroy calendar's events starting after date", async () => { + const calendar = new Calendar(); + await calendar.destroyEvents('07ec2599-3221-4d6c-ac56-41443973201b', { from: '2019-03-10 07:49:07.556 +00:00' }); + const allCalendarEvents = await calendar.getEvents( + '0cd30aef-9c4e-4a23-88e3-3547971296e5', + '07ec2599-3221-4d6c-ac56-41443973201b', + ); + expect(allCalendarEvents.length).eq(1); + }); }); describe('calendar.getEvents', () => { diff --git a/server/test/services/caldav/lib/calendar/syncUserCalendars.test.js b/server/test/services/caldav/lib/calendar/syncUserCalendars.test.js index 89b2d63168..50b0cc365e 100644 --- a/server/test/services/caldav/lib/calendar/syncUserCalendars.test.js +++ b/server/test/services/caldav/lib/calendar/syncUserCalendars.test.js @@ -22,6 +22,7 @@ describe('CalDAV sync', () => { syncUserCalendars, formatCalendars, formatEvents, + formatRecurringEvents: sinon.stub().returns([]), requestCalendars: sinon.stub(), requestChanges: sinon.stub(), requestEventsData: sinon.stub(), @@ -37,6 +38,7 @@ describe('CalDAV sync', () => { .stub() .withArgs('event-to-delete') .resolves(), + destroyEvents: sinon.stub().resolves(), }, variable: { getValue: sinon.stub(), @@ -181,6 +183,19 @@ describe('CalDAV sync', () => { start: new Date('2018-06-08 00:00:00.000 +00:00'), location: null, }, + { + type: 'VEVENT', + uid: '3a98f1eb-e8e9-4f09-8454-353e92f9ff0d', + summary: 'Evenement 4 to update with rrule', + start: new Date('2018-06-08 00:00:00.000 +00:00'), + location: null, + href: '/home/personal/event-4.ics', + rrule: { + options: { + until: '2019-06-08 00:00:00.000 +00:00', + }, + }, + }, ]); sync.gladys.calendar.getEvents @@ -251,6 +266,7 @@ describe('CalDAV sync', () => { expect(sync.gladys.calendar.update.callCount).to.equal(1); expect(sync.gladys.calendar.getEvents.callCount).to.equal(4); expect(sync.gladys.calendar.destroyEvent.callCount).to.equal(1); + expect(sync.gladys.calendar.destroyEvents.callCount).to.equal(1); }); it('should failed if no CALDAV_HOST', async () => {