Skip to content

Commit

Permalink
feat(ocp): add calendar api to retrieve availability of attendees
Browse files Browse the repository at this point in the history
Signed-off-by: Richard Steinmetz <[email protected]>
  • Loading branch information
st3iny committed Jan 13, 2025
1 parent dd0f7f0 commit 3dbdf32
Show file tree
Hide file tree
Showing 10 changed files with 457 additions and 0 deletions.
5 changes: 5 additions & 0 deletions apps/dav/lib/ServerFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,15 @@
namespace OCA\DAV;

use OCA\DAV\CalDAV\InvitationResponse\InvitationResponseServer;
use OCA\DAV\Connector\Sabre\Server;

class ServerFactory {

public function createInviationResponseServer(bool $public): InvitationResponseServer {
return new InvitationResponseServer(false);
}

public function createAttendeeAvailabilityServer(): Server {
return (new InvitationResponseServer(false))->getServer();
}
}
2 changes: 2 additions & 0 deletions lib/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down
2 changes: 2 additions & 0 deletions lib/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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',
Expand Down
28 changes: 28 additions & 0 deletions lib/private/Calendar/AvailabilityResult.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

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

namespace OC\Calendar;

use OCP\Calendar\IAvailabilityResult;

class AvailabilityResult implements IAvailabilityResult {
public function __construct(
private readonly string $attendee,
private readonly bool $available,
) {
}

public function getAttendeeEmail(): string {
return $this->attendee;
}

public function isAvailable(): bool {
return $this->available;
}
}
93 changes: 93 additions & 0 deletions lib/private/Calendar/Manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -20,11 +23,16 @@
use OCP\Calendar\ICreateFromString;
use OCP\Calendar\IHandleImipMessage;
use OCP\Calendar\IManager;
use OCP\IUser;
use OCP\IUserManager;
use OCP\Security\ISecureRandom;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Sabre\HTTP\Request;
use Sabre\HTTP\Response;
use Sabre\VObject\Component\VCalendar;
use Sabre\VObject\Component\VEvent;
use Sabre\VObject\Component\VFreeBusy;
use Sabre\VObject\Property\VCard\DateTime;
use Sabre\VObject\Reader;
use Throwable;
Expand All @@ -48,6 +56,8 @@ public function __construct(
private LoggerInterface $logger,
private ITimeFactory $timeFactory,
private ISecureRandom $random,
private IUserManager $userManager,
private ServerFactory $serverFactory,
) {
}

Expand Down Expand Up @@ -472,4 +482,87 @@ 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,
IUser $organizer,
array $attendees,
): array {
$organizerMailto = 'mailto:' . $organizer->getEMailAddress();
$request = new VCalendar();
$request->METHOD = 'REQUEST';
$request->add('VFREEBUSY', [
'DTSTART' => $start,
'DTEND' => $end,
'ORGANIZER' => $organizerMailto,
'ATTENDEE' => $organizerMailto,
]);

$mailtoLen = strlen('mailto:');
foreach ($attendees as $attendee) {
if (str_starts_with($attendee, 'mailto:')) {
$attendee = substr($attendee, $mailtoLen);
}

$attendeeUsers = $this->userManager->getByEmail($attendee);
if ($attendeeUsers === []) {
continue;
}

$request->VFREEBUSY->add('ATTENDEE', "mailto:$attendee");
}

$organizerUid = $organizer->getUID();
$server = $this->serverFactory->createAttendeeAvailabilityServer();
/** @var CustomPrincipalPlugin $plugin */
$plugin = $server->getPlugin('auth');
$plugin->setCurrentPrincipal("principals/users/$organizerUid");

$request = new Request(
'POST',
"/calendars/$organizerUid/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',
'{urn:ietf:params:xml:ns:caldav}recipient' => 'Sabre\Xml\Deserializer\keyValue',
];
$parsedResponse = $xmlService->parse($response->getBodyAsString());

$result = [];
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(
$freeBusyResponse['{urn:ietf:params:xml:ns:caldav}recipient']['{DAV:}href'],
$mailtoLen,
);

$vFreeBusy = $freeBusyResponseData->VFREEBUSY;
if (!($vFreeBusy instanceof VFreeBusy)) {
continue;
}

// TODO: actually check values of FREEBUSY properties to find a free slot
$result[] = new AvailabilityResult($attendee, $vFreeBusy->isFree($start, $end));
}

return $result;
}
}
32 changes: 32 additions & 0 deletions lib/public/Calendar/IAvailabilityResult.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

declare(strict_types=1);

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

namespace OCP\Calendar;

/**
* DTO for the availability check results.
* Holds information about whether an attendee is available or not during the request time slot.
*
* @since 31.0.0
*/
interface IAvailabilityResult {
/**
* Get the attendee's email address.
*
* @since 31.0.0
*/
public function getAttendeeEmail(): string;

/**
* Whether the attendee is available during the requested time slot.
*
* @since 31.0.0
*/
public function isAvailable(): bool;
}
19 changes: 19 additions & 0 deletions lib/public/Calendar/IManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
*/
namespace OCP\Calendar;

use DateTimeInterface;
use OCP\IUser;

/**
* This class provides access to the Nextcloud CalDAV backend.
* Use this class exclusively if you want to access calendars.
Expand Down Expand Up @@ -165,4 +168,20 @@ public function handleIMipCancel(string $principalUri, string $sender, ?string $
* @since 31.0.0
*/
public function createEventBuilder(): ICalendarEventBuilder;

/**
* Check the availability of the given organizer and attendees in the given time range.
*
* @since 31.0.0
*
* @param IUser $organizer The organizing user from whose perspective to do the availability check.
* @param string[] $attendees Email addresses of attendees to check for (with or without a "mailto:" prefix). Only users on this instance can be checked. The rest will be silently ignored.
* @return IAvailabilityResult[] Availabilities of the organizer and all attendees which are also users on this instance. As such, the array might not contain an entry for each given attendee.
*/
public function checkAvailability(
DateTimeInterface $start,
DateTimeInterface $end,
IUser $organizer,
array $attendees,
): array;
}
14 changes: 14 additions & 0 deletions tests/data/ics/free-busy-request.ics
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Sabre//Sabre VObject 4.5.6//EN
CALSCALE:GREGORIAN
METHOD:REQUEST
BEGIN:VFREEBUSY
DTSTART:20250116T060000Z
DTEND:20250117T060000Z
ORGANIZER:mailto:[email protected]
ATTENDEE:mailto:[email protected]
ATTENDEE:mailto:[email protected]
ATTENDEE:mailto:[email protected]
END:VFREEBUSY
END:VCALENDAR
2 changes: 2 additions & 0 deletions tests/data/ics/free-busy-request.ics.license
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
SPDX-License-Identifier: AGPL-3.0-or-later
Loading

0 comments on commit 3dbdf32

Please sign in to comment.