Skip to content

Commit

Permalink
feat: sync calendar instantly on changes
Browse files Browse the repository at this point in the history
Signed-off-by: Richard Steinmetz <[email protected]>
  • Loading branch information
st3iny committed Sep 23, 2024
1 parent 0b17aa8 commit 97948db
Show file tree
Hide file tree
Showing 7 changed files with 162 additions and 30 deletions.
8 changes: 8 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,14 @@
use OCA\Calendar\Events\BeforeAppointmentBookedEvent;
use OCA\Calendar\Listener\AppointmentBookedListener;
use OCA\Calendar\Listener\CalendarReferenceListener;
use OCA\Calendar\Listener\NotifyPushListener;
use OCA\Calendar\Listener\UserDeletedListener;
use OCA\Calendar\Notification\Notifier;
use OCA\Calendar\Profile\AppointmentsAction;
use OCA\Calendar\Reference\ReferenceProvider;
use OCA\DAV\Events\CalendarObjectCreatedEvent;
use OCA\DAV\Events\CalendarObjectDeletedEvent;
use OCA\DAV\Events\CalendarObjectUpdatedEvent;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
use OCP\AppFramework\Bootstrap\IBootstrap;
Expand Down Expand Up @@ -50,6 +54,10 @@ public function register(IRegistrationContext $context): void {
$context->registerEventListener(UserDeletedEvent::class, UserDeletedListener::class);
$context->registerEventListener(RenderReferenceEvent::class, CalendarReferenceListener::class);

$context->registerEventListener(CalendarObjectCreatedEvent::class, NotifyPushListener::class);

Check failure on line 57 in lib/AppInfo/Application.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-master

UndefinedClass

lib/AppInfo/Application.php:57:35: UndefinedClass: Class, interface or enum named OCA\DAV\Events\CalendarObjectCreatedEvent does not exist (see https://psalm.dev/019)

Check failure on line 57 in lib/AppInfo/Application.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-stable30

UndefinedClass

lib/AppInfo/Application.php:57:35: UndefinedClass: Class, interface or enum named OCA\DAV\Events\CalendarObjectCreatedEvent does not exist (see https://psalm.dev/019)
$context->registerEventListener(CalendarObjectUpdatedEvent::class, NotifyPushListener::class);

Check failure on line 58 in lib/AppInfo/Application.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-master

UndefinedClass

lib/AppInfo/Application.php:58:35: UndefinedClass: Class, interface or enum named OCA\DAV\Events\CalendarObjectUpdatedEvent does not exist (see https://psalm.dev/019)

Check failure on line 58 in lib/AppInfo/Application.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-stable30

UndefinedClass

lib/AppInfo/Application.php:58:35: UndefinedClass: Class, interface or enum named OCA\DAV\Events\CalendarObjectUpdatedEvent does not exist (see https://psalm.dev/019)
$context->registerEventListener(CalendarObjectDeletedEvent::class, NotifyPushListener::class);

Check failure on line 59 in lib/AppInfo/Application.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-master

UndefinedClass

lib/AppInfo/Application.php:59:35: UndefinedClass: Class, interface or enum named OCA\DAV\Events\CalendarObjectDeletedEvent does not exist (see https://psalm.dev/019)

Check failure on line 59 in lib/AppInfo/Application.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-stable30

UndefinedClass

lib/AppInfo/Application.php:59:35: UndefinedClass: Class, interface or enum named OCA\DAV\Events\CalendarObjectDeletedEvent does not exist (see https://psalm.dev/019)

$context->registerNotifierService(Notifier::class);
}

Expand Down
57 changes: 57 additions & 0 deletions lib/Listener/NotifyPushListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Calendar\Listener;

use OCA\DAV\Events\CalendarObjectCreatedEvent;
use OCA\DAV\Events\CalendarObjectDeletedEvent;
use OCA\DAV\Events\CalendarObjectUpdatedEvent;
use OCA\NotifyPush\Queue\IQueue;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\IURLGenerator;
use OCP\IUserSession;

/**
* @template-implements IEventListener<Event|>
*/
class NotifyPushListener implements IEventListener {
public function __construct(
private readonly IUserSession $userSession,
private readonly IURLGenerator $urlGenerator,
private readonly ?IQueue $queue,

Check failure on line 28 in lib/Listener/NotifyPushListener.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-master

UndefinedClass

lib/Listener/NotifyPushListener.php:28:3: UndefinedClass: Class, interface or enum named OCA\NotifyPush\Queue\IQueue does not exist (see https://psalm.dev/019)

Check failure on line 28 in lib/Listener/NotifyPushListener.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-stable30

UndefinedClass

lib/Listener/NotifyPushListener.php:28:3: UndefinedClass: Class, interface or enum named OCA\NotifyPush\Queue\IQueue does not exist (see https://psalm.dev/019)
) {
}

/**
* @param CalendarObjectCreatedEvent|CalendarObjectUpdatedEvent|CalendarObjectDeletedEvent> $event
*/
public function handle(Event $event): void {

Check failure on line 35 in lib/Listener/NotifyPushListener.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-master

InvalidDocblock

lib/Listener/NotifyPushListener.php:35:2: InvalidDocblock: Invalid string CalendarObjectCreatedEvent|CalendarObjectUpdatedEvent|CalendarObjectDeletedEvent> $event in docblock for OCA\Calendar\Listener\NotifyPushListener::handle (see https://psalm.dev/008)

Check failure on line 35 in lib/Listener/NotifyPushListener.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-stable30

InvalidDocblock

lib/Listener/NotifyPushListener.php:35:2: InvalidDocblock: Invalid string CalendarObjectCreatedEvent|CalendarObjectUpdatedEvent|CalendarObjectDeletedEvent> $event in docblock for OCA\Calendar\Listener\NotifyPushListener::handle (see https://psalm.dev/008)
if ($this->queue === null) {
return;
}

$user = $this->userSession->getUser();
if ($user === null) {
return;
}

// TODO: How to generate this in a more safe way?
$webroot = $this->urlGenerator->getWebroot();
$uid = $user->getUID();
$uri = $event->getCalendarData()['uri'];

Check failure on line 48 in lib/Listener/NotifyPushListener.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-master

UndefinedMethod

lib/Listener/NotifyPushListener.php:48:18: UndefinedMethod: Method OCP\EventDispatcher\Event::getCalendarData does not exist (see https://psalm.dev/022)

Check failure on line 48 in lib/Listener/NotifyPushListener.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-stable30

UndefinedMethod

lib/Listener/NotifyPushListener.php:48:18: UndefinedMethod: Method OCP\EventDispatcher\Event::getCalendarData does not exist (see https://psalm.dev/022)
$this->queue->push('notify_custom', [

Check failure on line 49 in lib/Listener/NotifyPushListener.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-master

UndefinedClass

lib/Listener/NotifyPushListener.php:49:3: UndefinedClass: Class, interface or enum named OCA\NotifyPush\Queue\IQueue does not exist (see https://psalm.dev/019)

Check failure on line 49 in lib/Listener/NotifyPushListener.php

View workflow job for this annotation

GitHub Actions / static-psalm-analysis dev-stable30

UndefinedClass

lib/Listener/NotifyPushListener.php:49:3: UndefinedClass: Class, interface or enum named OCA\NotifyPush\Queue\IQueue does not exist (see https://psalm.dev/019)
'user' => $user->getUID(),
'message' => 'calendar_sync',
'body' => [
'calendarUrl' => "$webroot/remote.php/dav/calendars/$uid/$uri/",
],
]);
}
}
12 changes: 12 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"@nextcloud/l10n": "^3.1.0",
"@nextcloud/logger": "^3.0.2",
"@nextcloud/moment": "^1.3.1",
"@nextcloud/notify_push": "^1.3.0",
"@nextcloud/router": "^3.0.1",
"@nextcloud/vue": "^8.18.0",
"autosize": "^6.0.1",
Expand Down
42 changes: 42 additions & 0 deletions src/services/notifyService.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { listen } from '@nextcloud/notify_push'
import { loadState } from '@nextcloud/initial-state'

Check failure on line 7 in src/services/notifyService.js

View workflow job for this annotation

GitHub Actions / NPM lint

'loadState' is defined but never used
import useCalendarsStore from '../store/calendars.js'
import logger from '../utils/logger.js'

/**
* Register a notify_push listener to listen for sync requests and sync calendars.
*/
export function registerNotifyPushSyncListener() {

Check warning on line 14 in src/services/notifyService.js

View check run for this annotation

Codecov / codecov/patch

src/services/notifyService.js#L14

Added line #L14 was not covered by tests
// TODO: how to actually get the state
/*
const isPushEnabled = loadState('calendar', 'notify_push_available', false)
if (!isPushEnabled) {
return
}
*/

const calendarsStore = useCalendarsStore()
listen('calendar_sync', (messageType, messageBody) => {
logger.debug('calendar_sync', {

Check warning on line 25 in src/services/notifyService.js

View check run for this annotation

Codecov / codecov/patch

src/services/notifyService.js#L23-L25

Added lines #L23 - L25 were not covered by tests
messageType,
messageBody,
})
const { calendarUrl } = messageBody
const calendar = calendarsStore.getCalendarByUrl(calendarUrl)

Check warning on line 30 in src/services/notifyService.js

View check run for this annotation

Codecov / codecov/patch

src/services/notifyService.js#L29-L30

Added lines #L29 - L30 were not covered by tests
if (!calendar) {
logger.warn(`Requested push sync for unknown calendar: ${calendarUrl}`, {

Check warning on line 32 in src/services/notifyService.js

View check run for this annotation

Codecov / codecov/patch

src/services/notifyService.js#L32

Added line #L32 was not covered by tests
messageType,
messageBody,
})
return

Check warning on line 36 in src/services/notifyService.js

View check run for this annotation

Codecov / codecov/patch

src/services/notifyService.js#L36

Added line #L36 was not covered by tests
}

logger.debug(`Syncing calendar ${calendarUrl} (requested by notify_push)`)
calendarsStore.syncCalendar({ calendar })

Check warning on line 40 in src/services/notifyService.js

View check run for this annotation

Codecov / codecov/patch

src/services/notifyService.js#L39-L40

Added lines #L39 - L40 were not covered by tests
})
}
38 changes: 38 additions & 0 deletions src/store/calendars.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import useSettingsStore from './settings.js'
import useFetchedTimeRangesStore from './fetchedTimeRanges.js'
import usePrincipalsStore from './principals.js'
import useCalendarObjectsStore from './calendarObjects.js'
import logger from '../utils/logger.js'

import { defineStore } from 'pinia'
import Vue from 'vue'
Expand Down Expand Up @@ -940,5 +941,42 @@ export default defineStore('calendars', {

this.syncTokens.set(calendar.id, syncToken)
},

syncCalendar({ calendar, skipIfUnchangedSyncToken = false }) {
const fetchedTimeRangesStore = useFetchedTimeRangesStore()
const calendarObjectsStore = useCalendarObjectsStore()
const calendarsStore = this

Check warning on line 948 in src/store/calendars.js

View check run for this annotation

Codecov / codecov/patch

src/store/calendars.js#L945-L948

Added lines #L945 - L948 were not covered by tests

const existingSyncToken = calendarsStore.getCalendarSyncToken(calendar)

Check warning on line 950 in src/store/calendars.js

View check run for this annotation

Codecov / codecov/patch

src/store/calendars.js#L950

Added line #L950 was not covered by tests
if (!existingSyncToken && !calendarsStore.getCalendarById(calendar.id)) {
// New calendar!
logger.debug(`Adding new calendar ${calendar.url}`)
calendarsStore.addCalendarMutation({ calendar })
return

Check warning on line 955 in src/store/calendars.js

View check run for this annotation

Codecov / codecov/patch

src/store/calendars.js#L953-L955

Added lines #L953 - L955 were not covered by tests
}

if (skipIfUnchangedSyncToken && calendar.dav.syncToken === existingSyncToken) {
return

Check warning on line 959 in src/store/calendars.js

View check run for this annotation

Codecov / codecov/patch

src/store/calendars.js#L959

Added line #L959 was not covered by tests
}

logger.debug(`Refetching calendar ${calendar.url} (syncToken changed)`)
const fetchedTimeRanges = fetchedTimeRangesStore

Check warning on line 963 in src/store/calendars.js

View check run for this annotation

Codecov / codecov/patch

src/store/calendars.js#L962-L963

Added lines #L962 - L963 were not covered by tests
.getAllTimeRangesForCalendar(calendar.id)
for (const timeRange of fetchedTimeRanges) {
fetchedTimeRangesStore.removeTimeRange({

Check warning on line 966 in src/store/calendars.js

View check run for this annotation

Codecov / codecov/patch

src/store/calendars.js#L965-L966

Added lines #L965 - L966 were not covered by tests
timeRangeId: timeRange.id,
})
calendarsStore.deleteFetchedTimeRangeFromCalendarMutation({

Check warning on line 969 in src/store/calendars.js

View check run for this annotation

Codecov / codecov/patch

src/store/calendars.js#L969

Added line #L969 was not covered by tests
calendar,
fetchedTimeRangeId: timeRange.id,
})
}

calendarsStore.updateCalendarSyncToken({

Check warning on line 975 in src/store/calendars.js

View check run for this annotation

Codecov / codecov/patch

src/store/calendars.js#L975

Added line #L975 was not covered by tests
calendar,
syncToken: calendar.dav.syncToken,
})
calendarObjectsStore.modificationCount++

Check warning on line 979 in src/store/calendars.js

View check run for this annotation

Codecov / codecov/patch

src/store/calendars.js#L979

Added line #L979 was not covered by tests
},
},
})
34 changes: 4 additions & 30 deletions src/views/Calendar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ import useSettingsStore from '../store/settings.js'
import useWidgetStore from '../store/widget.js'
import { mapStores, mapState } from 'pinia'
import { mapDavCollectionToCalendar } from '../models/calendar.js'
import { registerNotifyPushSyncListener } from '../services/notifyService.js'
export default {
name: 'Calendar',
Expand Down Expand Up @@ -210,41 +211,14 @@ export default {
},
},
created() {
registerNotifyPushSyncListener()

Check warning on line 214 in src/views/Calendar.vue

View check run for this annotation

Codecov / codecov/patch

src/views/Calendar.vue#L214

Added line #L214 was not covered by tests

Check failure on line 215 in src/views/Calendar.vue

View workflow job for this annotation

GitHub Actions / NPM lint

Trailing spaces not allowed
this.backgroundSyncJob = setInterval(async () => {
const currentUserPrincipal = this.principalsStore.getCurrentUserPrincipal
const calendars = (await findAllCalendars())
.map((calendar) => mapDavCollectionToCalendar(calendar, currentUserPrincipal))
for (const calendar of calendars) {
const existingSyncToken = this.calendarsStore.getCalendarSyncToken(calendar)
if (!existingSyncToken && !this.calendarsStore.getCalendarById(calendar.id)) {
// New calendar!
logger.debug(`Adding new calendar ${calendar.url}`)
this.calendarsStore.addCalendarMutation({ calendar })
continue
}
if (calendar.dav.syncToken === existingSyncToken) {
continue
}
logger.debug(`Refetching calendar ${calendar.url} (syncToken changed)`)
const fetchedTimeRanges = this.fetchedTimeRangesStore
.getAllTimeRangesForCalendar(calendar.id)
for (const timeRange of fetchedTimeRanges) {
this.fetchedTimeRangesStore.removeTimeRange({
timeRangeId: timeRange.id,
})
this.calendarsStore.deleteFetchedTimeRangeFromCalendarMutation({
calendar,
fetchedTimeRangeId: timeRange.id,
})
}
this.calendarsStore.updateCalendarSyncToken({
calendar,
syncToken: calendar.dav.syncToken,
})
this.calendarObjectsStore.modificationCount++
this.calendarsStore.syncCalendar({ calendar, skipIfUnchangedSyncToken: true })
}
}, 1000 * 30)
Expand Down

0 comments on commit 97948db

Please sign in to comment.