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 8, 2025
1 parent dd0f7f0 commit 1ddb7d7
Show file tree
Hide file tree
Showing 7 changed files with 149 additions and 0 deletions.
4 changes: 4 additions & 0 deletions apps/dav/lib/ServerFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
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;
}
}
88 changes: 88 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 @@ -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;
Expand All @@ -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,
) {
}

Expand Down Expand Up @@ -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);

Check failure on line 553 in lib/private/Calendar/Manager.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

UndefinedPropertyFetch

lib/private/Calendar/Manager.php:553:31: UndefinedPropertyFetch: Instance property Sabre\VObject\Property::$ATTENDEE is not defined (see https://psalm.dev/039)
/** @var DateTimeInterface $attendeeFreeStarting */
$attendeeFreeStarting = $freeBusyResponseData->VFREEBUSY->DTSTART->getDateTime();

Check failure on line 555 in lib/private/Calendar/Manager.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

UndefinedPropertyFetch

lib/private/Calendar/Manager.php:555:28: UndefinedPropertyFetch: Instance property Sabre\VObject\Property::$DTSTART is not defined (see https://psalm.dev/039)
// TODO: use DTSTART and DTEND to get the next free slot
$isAvailable = $attendeeFreeStarting->getTimestamp() <= $start->getTimestamp();
$result[] = new AvailabilityResult($attendee, $isAvailable);
}

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

declare(strict_types=1);

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

namespace OCP\Calendar;

interface IAvailabilityResult {

Check failure on line 12 in lib/public/Calendar/IAvailabilityResult.php

View workflow job for this annotation

GitHub Actions / static-code-analysis-ocp

InvalidDocblock

lib/public/Calendar/IAvailabilityResult.php:12:1: InvalidDocblock: PHPDoc is required for classes/interfaces in OCP. (see https://psalm.dev/008)
public function getAttendeeEmail(): string;
public function isAvailable(): bool;
}
10 changes: 10 additions & 0 deletions lib/public/Calendar/IManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -165,4 +165,14 @@ public function handleIMipCancel(string $principalUri, string $sender, ?string $
* @since 31.0.0
*/
public function createEventBuilder(): ICalendarEventBuilder;

/**
* Check the availability of the given attendees in the given time range.
*
* @since 31.0.0
*
* @param string[] $attendees Email addresses of attendees to check for. Only users on this instance can be checked. The rest will be ignored.
* @return IAvailabilityResult[]
*/
public function checkAvailability(\DateTimeInterface $start, \DateTimeInterface $end, array $attendees): array;
}

0 comments on commit 1ddb7d7

Please sign in to comment.