Skip to content

Commit 5b7a547

Browse files
feat(caldav): Create personal event for out-of-office messages
Signed-off-by: Christoph Wurst <christoph@winzerhof-wurst.at>
1 parent f313b12 commit 5b7a547

File tree

9 files changed

+819
-5
lines changed

9 files changed

+819
-5
lines changed

apps/dav/composer/composer/autoload_classmap.php

+2
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,7 @@
265265
'OCA\\DAV\\Listener\\CalendarShareUpdateListener' => $baseDir . '/../lib/Listener/CalendarShareUpdateListener.php',
266266
'OCA\\DAV\\Listener\\CardListener' => $baseDir . '/../lib/Listener/CardListener.php',
267267
'OCA\\DAV\\Listener\\ClearPhotoCacheListener' => $baseDir . '/../lib/Listener/ClearPhotoCacheListener.php',
268+
'OCA\\DAV\\Listener\\OutOfOfficeListener' => $baseDir . '/../lib/Listener/OutOfOfficeListener.php',
268269
'OCA\\DAV\\Listener\\SubscriptionListener' => $baseDir . '/../lib/Listener/SubscriptionListener.php',
269270
'OCA\\DAV\\Listener\\TrustedServerRemovedListener' => $baseDir . '/../lib/Listener/TrustedServerRemovedListener.php',
270271
'OCA\\DAV\\Listener\\UserPreferenceListener' => $baseDir . '/../lib/Listener/UserPreferenceListener.php',
@@ -316,6 +317,7 @@
316317
'OCA\\DAV\\Search\\EventsSearchProvider' => $baseDir . '/../lib/Search/EventsSearchProvider.php',
317318
'OCA\\DAV\\Search\\TasksSearchProvider' => $baseDir . '/../lib/Search/TasksSearchProvider.php',
318319
'OCA\\DAV\\Server' => $baseDir . '/../lib/Server.php',
320+
'OCA\\DAV\\ServerFactory' => $baseDir . '/../lib/ServerFactory.php',
319321
'OCA\\DAV\\Service\\AbsenceService' => $baseDir . '/../lib/Service/AbsenceService.php',
320322
'OCA\\DAV\\Settings\\AvailabilitySettings' => $baseDir . '/../lib/Settings/AvailabilitySettings.php',
321323
'OCA\\DAV\\Settings\\CalDAVSettings' => $baseDir . '/../lib/Settings/CalDAVSettings.php',

apps/dav/composer/composer/autoload_static.php

+2
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,7 @@ class ComposerStaticInitDAV
280280
'OCA\\DAV\\Listener\\CalendarShareUpdateListener' => __DIR__ . '/..' . '/../lib/Listener/CalendarShareUpdateListener.php',
281281
'OCA\\DAV\\Listener\\CardListener' => __DIR__ . '/..' . '/../lib/Listener/CardListener.php',
282282
'OCA\\DAV\\Listener\\ClearPhotoCacheListener' => __DIR__ . '/..' . '/../lib/Listener/ClearPhotoCacheListener.php',
283+
'OCA\\DAV\\Listener\\OutOfOfficeListener' => __DIR__ . '/..' . '/../lib/Listener/OutOfOfficeListener.php',
283284
'OCA\\DAV\\Listener\\SubscriptionListener' => __DIR__ . '/..' . '/../lib/Listener/SubscriptionListener.php',
284285
'OCA\\DAV\\Listener\\TrustedServerRemovedListener' => __DIR__ . '/..' . '/../lib/Listener/TrustedServerRemovedListener.php',
285286
'OCA\\DAV\\Listener\\UserPreferenceListener' => __DIR__ . '/..' . '/../lib/Listener/UserPreferenceListener.php',
@@ -331,6 +332,7 @@ class ComposerStaticInitDAV
331332
'OCA\\DAV\\Search\\EventsSearchProvider' => __DIR__ . '/..' . '/../lib/Search/EventsSearchProvider.php',
332333
'OCA\\DAV\\Search\\TasksSearchProvider' => __DIR__ . '/..' . '/../lib/Search/TasksSearchProvider.php',
333334
'OCA\\DAV\\Server' => __DIR__ . '/..' . '/../lib/Server.php',
335+
'OCA\\DAV\\ServerFactory' => __DIR__ . '/..' . '/../lib/ServerFactory.php',
334336
'OCA\\DAV\\Service\\AbsenceService' => __DIR__ . '/..' . '/../lib/Service/AbsenceService.php',
335337
'OCA\\DAV\\Settings\\AvailabilitySettings' => __DIR__ . '/..' . '/../lib/Settings/AvailabilitySettings.php',
336338
'OCA\\DAV\\Settings\\CalDAVSettings' => __DIR__ . '/..' . '/../lib/Settings/CalDAVSettings.php',

apps/dav/lib/AppInfo/Application.php

+8
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@
6969
use OCA\DAV\Events\CardUpdatedEvent;
7070
use OCA\DAV\Events\SubscriptionCreatedEvent;
7171
use OCA\DAV\Events\SubscriptionDeletedEvent;
72+
use OCA\DAV\Listener\OutOfOfficeListener;
7273
use OCP\Accounts\UserUpdatedEvent;
7374
use OCP\EventDispatcher\IEventDispatcher;
7475
use OCP\Federation\Events\TrustedServerRemovedEvent;
@@ -103,6 +104,9 @@
103104
use OCP\Contacts\IManager as IContactsManager;
104105
use OCP\Files\AppData\IAppDataFactory;
105106
use OCP\IUser;
107+
use OCP\User\Events\OutOfOfficeChangedEvent;
108+
use OCP\User\Events\OutOfOfficeClearedEvent;
109+
use OCP\User\Events\OutOfOfficeScheduledEvent;
106110
use Psr\Container\ContainerInterface;
107111
use Psr\Log\LoggerInterface;
108112
use Symfony\Component\EventDispatcher\GenericEvent;
@@ -195,6 +199,10 @@ public function register(IRegistrationContext $context): void {
195199
$context->registerEventListener(BeforePreferenceDeletedEvent::class, UserPreferenceListener::class);
196200
$context->registerEventListener(BeforePreferenceSetEvent::class, UserPreferenceListener::class);
197201

202+
$context->registerEventListener(OutOfOfficeChangedEvent::class, OutOfOfficeListener::class);
203+
$context->registerEventListener(OutOfOfficeClearedEvent::class, OutOfOfficeListener::class);
204+
$context->registerEventListener(OutOfOfficeScheduledEvent::class, OutOfOfficeListener::class);
205+
198206
$context->registerNotifierService(Notifier::class);
199207

200208
$context->registerCalendarProvider(CalendarProvider::class);

apps/dav/lib/Db/Absence.php

+4-1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
namespace OCA\DAV\Db;
2828

2929
use DateTimeImmutable;
30+
use Exception;
3031
use InvalidArgumentException;
3132
use JsonSerializable;
3233
use OC\User\OutOfOfficeData;
@@ -70,8 +71,10 @@ public function toOutOufOfficeData(IUser $user): IOutOfOfficeData {
7071
if ($user->getUID() !== $this->getUserId()) {
7172
throw new InvalidArgumentException("The user doesn't match the user id of this absence! Expected " . $this->getUserId() . ", got " . $user->getUID());
7273
}
74+
if ($this->getId() === null) {
75+
throw new Exception('Creating out-of-office data without ID');
76+
}
7377

74-
//$user = $userManager->get($this->getUserId());
7578
$startDate = new DateTimeImmutable($this->getFirstDay());
7679
$endDate = new DateTimeImmutable($this->getLastDay());
7780
return new OutOfOfficeData(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
7+
*
8+
* @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
9+
*
10+
* @license GNU AGPL version 3 or any later version
11+
*
12+
* This program is free software: you can redistribute it and/or modify
13+
* it under the terms of the GNU Affero General Public License as
14+
* published by the Free Software Foundation, either version 3 of the
15+
* License, or (at your option) any later version.
16+
*
17+
* This program is distributed in the hope that it will be useful,
18+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
19+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20+
* GNU Affero General Public License for more details.
21+
*
22+
* You should have received a copy of the GNU Affero General Public License
23+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
24+
*/
25+
26+
namespace OCA\DAV\Listener;
27+
28+
use DateTimeImmutable;
29+
use OCA\DAV\CalDAV\CalDavBackend;
30+
use OCA\DAV\CalDAV\Calendar;
31+
use OCA\DAV\CalDAV\CalendarHome;
32+
use OCA\DAV\ServerFactory;
33+
use OCP\EventDispatcher\Event;
34+
use OCP\EventDispatcher\IEventListener;
35+
use OCP\IConfig;
36+
use OCP\User\Events\OutOfOfficeChangedEvent;
37+
use OCP\User\Events\OutOfOfficeClearedEvent;
38+
use OCP\User\Events\OutOfOfficeScheduledEvent;
39+
use OCP\User\IOutOfOfficeData;
40+
use Psr\Log\LoggerInterface;
41+
use Sabre\DAV\Exception\NotFound;
42+
use Sabre\VObject\Component\VCalendar;
43+
use Sabre\VObject\Component\VEvent;
44+
use Sabre\VObject\Component\VTimeZone;
45+
use Sabre\VObject\Reader;
46+
use function fclose;
47+
use function fopen;
48+
use function fwrite;
49+
use function rewind;
50+
51+
/**
52+
* @template-implements IEventListener<OutOfOfficeScheduledEvent|OutOfOfficeChangedEvent|OutOfOfficeClearedEvent>
53+
*/
54+
class OutOfOfficeListener implements IEventListener {
55+
public function __construct(private ServerFactory $serverFactory,
56+
private IConfig $appConfig,
57+
private LoggerInterface $logger) {
58+
}
59+
60+
public function handle(Event $event): void {
61+
if ($event instanceof OutOfOfficeScheduledEvent) {
62+
$userId = $event->getData()->getUser()->getUID();
63+
$principal = "principals/users/$userId";
64+
65+
$calendarNode = $this->getCalendarNode($principal, $userId);
66+
if ($calendarNode === null) {
67+
return;
68+
}
69+
70+
$tz = $calendarNode->getProperties([])['{urn:ietf:params:xml:ns:caldav}calendar-timezone'] ?? null;
71+
$vCalendarEvent = $this->createVCalendarEvent($event->getData(), $tz);
72+
$stream = fopen('php://memory', 'rb+');
73+
try {
74+
fwrite($stream, $vCalendarEvent->serialize());
75+
rewind($stream);
76+
$calendarNode->createFile(
77+
$this->getEventFileName($event->getData()->getId()),
78+
$stream,
79+
);
80+
} finally {
81+
fclose($stream);
82+
}
83+
} else if ($event instanceof OutOfOfficeChangedEvent) {
84+
$userId = $event->getData()->getUser()->getUID();
85+
$principal = "principals/users/$userId";
86+
87+
$calendarNode = $this->getCalendarNode($principal, $userId);
88+
if ($calendarNode === null) {
89+
return;
90+
}
91+
$tz = $calendarNode->getProperties([])['{urn:ietf:params:xml:ns:caldav}calendar-timezone'] ?? null;
92+
$vCalendarEvent = $this->createVCalendarEvent($event->getData(), $tz);
93+
try {
94+
$oldEvent = $calendarNode->getChild($this->getEventFileName($event->getData()->getId()));
95+
$oldEvent->put($vCalendarEvent->serialize());
96+
return;
97+
} catch (NotFound) {
98+
$stream = fopen('php://memory', 'rb+');
99+
try {
100+
fwrite($stream, $vCalendarEvent->serialize());
101+
rewind($stream);
102+
$calendarNode->createFile(
103+
$this->getEventFileName($event->getData()->getId()),
104+
$stream,
105+
);
106+
} finally {
107+
fclose($stream);
108+
}
109+
}
110+
} else if ($event instanceof OutOfOfficeClearedEvent) {
111+
$userId = $event->getData()->getUser()->getUID();
112+
$principal = "principals/users/$userId";
113+
114+
$calendarNode = $this->getCalendarNode($principal, $userId);
115+
if ($calendarNode === null) {
116+
return;
117+
}
118+
119+
try {
120+
$oldEvent = $calendarNode->getChild($this->getEventFileName($event->getData()->getId()));
121+
$oldEvent->delete();
122+
} catch (NotFound) {
123+
// The user must have deleted it or the default calendar changed -> ignore
124+
}
125+
}
126+
}
127+
128+
private function getCalendarNode(string $principal, string $userId): ?Calendar {
129+
$invitationServer = $this->serverFactory->createInviationResponseServer(false);
130+
$server = $invitationServer->getServer();
131+
132+
/** @var \OCA\DAV\CalDAV\Plugin $caldavPlugin */
133+
$caldavPlugin = $server->getPlugin('caldav');
134+
$calendarHomePath = $caldavPlugin->getCalendarHomeForPrincipal($principal);
135+
if ($calendarHomePath === null) {
136+
$this->logger->debug('Principal has no calendar home path');
137+
return null;
138+
}
139+
try {
140+
/** @var CalendarHome $calendarHome */
141+
$calendarHome = $server->tree->getNodeForPath($calendarHomePath);
142+
} catch (NotFound $e) {
143+
$this->logger->debug('Calendar home not found', [
144+
'exception' => $e,
145+
]);
146+
return null;
147+
}
148+
$uri = $this->appConfig->getUserValue($userId, 'dav', 'defaultCalendar', CalDavBackend::PERSONAL_CALENDAR_URI);
149+
try {
150+
$calendarNode = $calendarHome->getChild($uri);
151+
} catch (NotFound $e) {
152+
$this->logger->debug('Personal calendar does not exist', [
153+
'exception' => $e,
154+
'uri' => $uri,
155+
]);
156+
return null;
157+
}
158+
if (!($calendarNode instanceof Calendar)) {
159+
$this->logger->warning('Personal calendar node is not a calendar');
160+
return null;
161+
}
162+
if ($calendarNode->isDeleted()) {
163+
$this->logger->warning('Personal calendar has been deleted');
164+
return null;
165+
}
166+
167+
return $calendarNode;
168+
}
169+
170+
private function getEventFileName(string $id): string {
171+
return "out_of_office_$id.ics";
172+
}
173+
174+
private function createVCalendarEvent(IOutOfOfficeData $data, ?string $timeZoneData): VCalendar {
175+
$shortMessage = $data->getShortMessage();
176+
$longMessage = $data->getMessage();
177+
$start = (new DateTimeImmutable)
178+
->setTimestamp($data->getStartDate())
179+
->setTime(0, 0);
180+
$end = (new DateTimeImmutable())
181+
->setTimestamp($data->getEndDate())
182+
->modify('+ 2 days')
183+
->setTime(0, 0);
184+
$vCalendar = new VCalendar();
185+
$vCalendar->add('VEVENT', [
186+
'SUMMARY' => $shortMessage,
187+
'DESCRIPTION' => $longMessage,
188+
'STATUS' => 'CONFIRMED',
189+
'DTSTART' => $start,
190+
'DTEND' => $end,
191+
'X-NEXTCLOUD-OUT-OF-OFFICE' => $data->getId(),
192+
]);
193+
/** @var VEvent $vEvent */
194+
$vEvent = $vCalendar->VEVENT;
195+
if ($timeZoneData !== null) {
196+
/** @var VCalendar $vtimezoneObj */
197+
$vtimezoneObj = Reader::read($timeZoneData);
198+
/** @var VTimeZone $vtimezone */
199+
$vtimezone = $vtimezoneObj->VTIMEZONE;
200+
$calendarTimeZone = $vtimezone->getTimeZone();
201+
$vCalendar->add($vtimezone);
202+
203+
/** @psalm-suppress UndefinedMethod */
204+
$vEvent->DTSTART->setDateTime($start->setTimezone($calendarTimeZone)->setTime(0, 0));
205+
/** @psalm-suppress UndefinedMethod */
206+
$vEvent->DTEND->setDateTime($end->setTimezone($calendarTimeZone)->setTime(0, 0));
207+
}
208+
return $vCalendar;
209+
}
210+
}

apps/dav/lib/Server.php

+4
Original file line numberDiff line numberDiff line change
@@ -385,4 +385,8 @@ private function requestIsForSubtree(array $subTrees): bool {
385385
}
386386
return false;
387387
}
388+
389+
public function getSabreServer(): Connector\Sabre\Server {
390+
return $this->server;
391+
}
388392
}

apps/dav/lib/ServerFactory.php

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* @copyright 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
7+
*
8+
* @author 2023 Christoph Wurst <christoph@winzerhof-wurst.at>
9+
*
10+
* @license GNU AGPL version 3 or any later version
11+
*
12+
* This program is free software: you can redistribute it and/or modify
13+
* it under the terms of the GNU Affero General Public License as
14+
* published by the Free Software Foundation, either version 3 of the
15+
* License, or (at your option) any later version.
16+
*
17+
* This program is distributed in the hope that it will be useful,
18+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
19+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20+
* GNU Affero General Public License for more details.
21+
*
22+
* You should have received a copy of the GNU Affero General Public License
23+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
24+
*/
25+
26+
namespace OCA\DAV;
27+
28+
use OCA\DAV\CalDAV\InvitationResponse\InvitationResponseServer;
29+
30+
class ServerFactory {
31+
32+
public function createInviationResponseServer(bool $public): InvitationResponseServer {
33+
return new InvitationResponseServer(false);
34+
}
35+
}

apps/dav/lib/Service/AbsenceService.php

+8-4
Original file line numberDiff line numberDiff line change
@@ -75,14 +75,18 @@ public function createOrUpdateAbsence(
7575
if ($user === null) {
7676
throw new InvalidArgumentException("User $userId does not exist");
7777
}
78-
$eventData = $absence->toOutOufOfficeData($user);
7978

8079
if ($absence->getId() === null) {
81-
$this->eventDispatcher->dispatchTyped(new OutOfOfficeScheduledEvent($eventData));
82-
return $this->absenceMapper->insert($absence);
80+
$persistedAbsence = $this->absenceMapper->insert($absence);
81+
$this->eventDispatcher->dispatchTyped(new OutOfOfficeScheduledEvent(
82+
$persistedAbsence->toOutOufOfficeData($user)
83+
));
84+
return $persistedAbsence;
8385
}
8486

85-
$this->eventDispatcher->dispatchTyped(new OutOfOfficeChangedEvent($eventData));
87+
$this->eventDispatcher->dispatchTyped(new OutOfOfficeChangedEvent(
88+
$absence->toOutOufOfficeData($user)
89+
));
8690
return $this->absenceMapper->update($absence);
8791
}
8892

0 commit comments

Comments
 (0)