From 1ddb7d72434256aea9f6831402b63db3a6ce4672 Mon Sep 17 00:00:00 2001 From: Richard Steinmetz Date: Wed, 8 Jan 2025 21:08:15 +0100 Subject: [PATCH] feat(ocp): add calendar api to retrieve availability of attendees Signed-off-by: Richard Steinmetz --- apps/dav/lib/ServerFactory.php | 4 + lib/composer/composer/autoload_classmap.php | 2 + lib/composer/composer/autoload_static.php | 2 + lib/private/Calendar/AvailabilityResult.php | 28 +++++++ lib/private/Calendar/Manager.php | 88 +++++++++++++++++++++ lib/public/Calendar/IAvailabilityResult.php | 15 ++++ lib/public/Calendar/IManager.php | 10 +++ 7 files changed, 149 insertions(+) create mode 100644 lib/private/Calendar/AvailabilityResult.php create mode 100644 lib/public/Calendar/IAvailabilityResult.php diff --git a/apps/dav/lib/ServerFactory.php b/apps/dav/lib/ServerFactory.php index 7a3f0b277479b..ecb148883385c 100644 --- a/apps/dav/lib/ServerFactory.php +++ b/apps/dav/lib/ServerFactory.php @@ -16,4 +16,8 @@ class ServerFactory { public function createInviationResponseServer(bool $public): InvitationResponseServer { return new InvitationResponseServer(false); } + + public function createAttendeeAvailabilityServer(): \OCA\DAV\Connector\Sabre\Server { + return (new InvitationResponseServer(false))->getServer(); + } } diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 200e2e75612c6..ffa8da43873ee 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -191,6 +191,7 @@ 'OCP\\Cache\\CappedMemoryCache' => $baseDir . '/lib/public/Cache/CappedMemoryCache.php', 'OCP\\Calendar\\BackendTemporarilyUnavailableException' => $baseDir . '/lib/public/Calendar/BackendTemporarilyUnavailableException.php', 'OCP\\Calendar\\Exceptions\\CalendarException' => $baseDir . '/lib/public/Calendar/Exceptions/CalendarException.php', + 'OCP\\Calendar\\IAvailabilityResult' => $baseDir . '/lib/public/Calendar/IAvailabilityResult.php', 'OCP\\Calendar\\ICalendar' => $baseDir . '/lib/public/Calendar/ICalendar.php', 'OCP\\Calendar\\ICalendarEventBuilder' => $baseDir . '/lib/public/Calendar/ICalendarEventBuilder.php', 'OCP\\Calendar\\ICalendarIsShared' => $baseDir . '/lib/public/Calendar/ICalendarIsShared.php', @@ -1117,6 +1118,7 @@ 'OC\\Broadcast\\Events\\BroadcastEvent' => $baseDir . '/lib/private/Broadcast/Events/BroadcastEvent.php', 'OC\\Cache\\CappedMemoryCache' => $baseDir . '/lib/private/Cache/CappedMemoryCache.php', 'OC\\Cache\\File' => $baseDir . '/lib/private/Cache/File.php', + 'OC\\Calendar\\AvailabilityResult' => $baseDir . '/lib/private/Calendar/AvailabilityResult.php', 'OC\\Calendar\\CalendarEventBuilder' => $baseDir . '/lib/private/Calendar/CalendarEventBuilder.php', 'OC\\Calendar\\CalendarQuery' => $baseDir . '/lib/private/Calendar/CalendarQuery.php', 'OC\\Calendar\\Manager' => $baseDir . '/lib/private/Calendar/Manager.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index bf9385c1741ca..89c0cd4395b37 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -232,6 +232,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OCP\\Cache\\CappedMemoryCache' => __DIR__ . '/../../..' . '/lib/public/Cache/CappedMemoryCache.php', 'OCP\\Calendar\\BackendTemporarilyUnavailableException' => __DIR__ . '/../../..' . '/lib/public/Calendar/BackendTemporarilyUnavailableException.php', 'OCP\\Calendar\\Exceptions\\CalendarException' => __DIR__ . '/../../..' . '/lib/public/Calendar/Exceptions/CalendarException.php', + 'OCP\\Calendar\\IAvailabilityResult' => __DIR__ . '/../../..' . '/lib/public/Calendar/IAvailabilityResult.php', 'OCP\\Calendar\\ICalendar' => __DIR__ . '/../../..' . '/lib/public/Calendar/ICalendar.php', 'OCP\\Calendar\\ICalendarEventBuilder' => __DIR__ . '/../../..' . '/lib/public/Calendar/ICalendarEventBuilder.php', 'OCP\\Calendar\\ICalendarIsShared' => __DIR__ . '/../../..' . '/lib/public/Calendar/ICalendarIsShared.php', @@ -1158,6 +1159,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Broadcast\\Events\\BroadcastEvent' => __DIR__ . '/../../..' . '/lib/private/Broadcast/Events/BroadcastEvent.php', 'OC\\Cache\\CappedMemoryCache' => __DIR__ . '/../../..' . '/lib/private/Cache/CappedMemoryCache.php', 'OC\\Cache\\File' => __DIR__ . '/../../..' . '/lib/private/Cache/File.php', + 'OC\\Calendar\\AvailabilityResult' => __DIR__ . '/../../..' . '/lib/private/Calendar/AvailabilityResult.php', 'OC\\Calendar\\CalendarEventBuilder' => __DIR__ . '/../../..' . '/lib/private/Calendar/CalendarEventBuilder.php', 'OC\\Calendar\\CalendarQuery' => __DIR__ . '/../../..' . '/lib/private/Calendar/CalendarQuery.php', 'OC\\Calendar\\Manager' => __DIR__ . '/../../..' . '/lib/private/Calendar/Manager.php', diff --git a/lib/private/Calendar/AvailabilityResult.php b/lib/private/Calendar/AvailabilityResult.php new file mode 100644 index 0000000000000..8031758f64e7e --- /dev/null +++ b/lib/private/Calendar/AvailabilityResult.php @@ -0,0 +1,28 @@ +attendee; + } + + public function isAvailable(): bool { + return $this->available; + } +} diff --git a/lib/private/Calendar/Manager.php b/lib/private/Calendar/Manager.php index 3469193a364d1..2fd0c951f0aa0 100644 --- a/lib/private/Calendar/Manager.php +++ b/lib/private/Calendar/Manager.php @@ -8,7 +8,10 @@ */ namespace OC\Calendar; +use DateTimeInterface; use OC\AppFramework\Bootstrap\Coordinator; +use OCA\DAV\CalDAV\Auth\CustomPrincipalPlugin; +use OCA\DAV\ServerFactory; use OCP\AppFramework\Utility\ITimeFactory; use OCP\Calendar\Exceptions\CalendarException; use OCP\Calendar\ICalendar; @@ -21,8 +24,13 @@ use OCP\Calendar\IHandleImipMessage; use OCP\Calendar\IManager; use OCP\Security\ISecureRandom; +use OCP\IUserManager; +use OCP\IUserSession; use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; +use Sabre\DAV\Exception\Forbidden; +use Sabre\HTTP\Request; +use Sabre\HTTP\Response; use Sabre\VObject\Component\VCalendar; use Sabre\VObject\Component\VEvent; use Sabre\VObject\Property\VCard\DateTime; @@ -48,6 +56,9 @@ public function __construct( private LoggerInterface $logger, private ITimeFactory $timeFactory, private ISecureRandom $random, + private IUserSession $userSession, + private IUserManager $userManager, + private ServerFactory $serverFactory, ) { } @@ -472,4 +483,81 @@ public function createEventBuilder(): ICalendarEventBuilder { $uid = $this->random->generate(32, ISecureRandom::CHAR_ALPHANUMERIC); return new CalendarEventBuilder($uid, $this->timeFactory); } + + public function checkAvailability( + DateTimeInterface $start, + DateTimeInterface $end, + array $attendees, + ): array { + $user = $this->userSession->getUser(); + if ($user === null) { + // Should never happen but just to be sure + throw new Forbidden(); + } + + $organizer = $user->getEMailAddress(); + + $request = new VCalendar(); + $request->METHOD = 'REQUEST'; + $request->add('VFREEBUSY', [ + 'DTSTART' => $start, + 'DTEND' => $end, + 'ORGANIZER' => "mailto:$organizer" + ]); + + foreach ($attendees as $attendee) { + $attendeeUsers = $this->userManager->getByEmail($attendee); + if ($attendeeUsers === []) { + continue; + } + + $request->VFREEBUSY->add('ATTENDEE', "mailto:$attendee"); + } + + $userId = $user->getUID(); + $server = $this->serverFactory->createAttendeeAvailabilityServer(); + /** @var CustomPrincipalPlugin $plugin */ + $plugin = $server->getPlugin('auth'); + $plugin->setCurrentPrincipal("principals/users/$userId"); + + $request = new Request( + 'POST', + "/calendars/$userId/outbox/", + [ + 'Content-Type' => 'text/calendar', + 'Depth' => 0, + ], + $request->serialize(), + ); + $response = new Response(); + $server->invokeMethod($request, $response, false); + + $xmlService = new \Sabre\Xml\Service(); + $xmlService->elementMap = [ + '{urn:ietf:params:xml:ns:caldav}response' => 'Sabre\Xml\Deserializer\keyValue', + ]; + $parsedResponse = $xmlService->parse($response->getBody()); + + $result = []; + $mailtoLen = strlen('mailto:'); + foreach ($parsedResponse as $freeBusyResponse) { + $freeBusyResponse = $freeBusyResponse['value']; + if ($freeBusyResponse['{urn:ietf:params:xml:ns:caldav}request-status'] !== '2.0;Success') { + continue; + } + + $freeBusyResponseData = \Sabre\VObject\Reader::read( + $freeBusyResponse['{urn:ietf:params:xml:ns:caldav}calendar-data'] + ); + + $attendee = substr((string)$freeBusyResponseData->VFREEBUSY->ATTENDEE, $mailtoLen); + /** @var DateTimeInterface $attendeeFreeStarting */ + $attendeeFreeStarting = $freeBusyResponseData->VFREEBUSY->DTSTART->getDateTime(); + // TODO: use DTSTART and DTEND to get the next free slot + $isAvailable = $attendeeFreeStarting->getTimestamp() <= $start->getTimestamp(); + $result[] = new AvailabilityResult($attendee, $isAvailable); + } + + return $result; + } } diff --git a/lib/public/Calendar/IAvailabilityResult.php b/lib/public/Calendar/IAvailabilityResult.php new file mode 100644 index 0000000000000..dd57421181d4e --- /dev/null +++ b/lib/public/Calendar/IAvailabilityResult.php @@ -0,0 +1,15 @@ +