Skip to content

Commit

Permalink
Merge pull request #49888 from nextcloud/feat/ocp/meetings-api-requir…
Browse files Browse the repository at this point in the history
…ements

feat(ocp): calendar event builder api
  • Loading branch information
nickvergessen authored Jan 8, 2025
2 parents e7122a6 + 42fa3ab commit dd0f7f0
Show file tree
Hide file tree
Showing 12 changed files with 489 additions and 27 deletions.
2 changes: 2 additions & 0 deletions lib/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,7 @@
'OCP\\Calendar\\BackendTemporarilyUnavailableException' => $baseDir . '/lib/public/Calendar/BackendTemporarilyUnavailableException.php',
'OCP\\Calendar\\Exceptions\\CalendarException' => $baseDir . '/lib/public/Calendar/Exceptions/CalendarException.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',
'OCP\\Calendar\\ICalendarIsWritable' => $baseDir . '/lib/public/Calendar/ICalendarIsWritable.php',
'OCP\\Calendar\\ICalendarProvider' => $baseDir . '/lib/public/Calendar/ICalendarProvider.php',
Expand Down Expand Up @@ -1116,6 +1117,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\\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',
'OC\\Calendar\\Resource\\Manager' => $baseDir . '/lib/private/Calendar/Resource/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 @@ -233,6 +233,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
'OCP\\Calendar\\BackendTemporarilyUnavailableException' => __DIR__ . '/../../..' . '/lib/public/Calendar/BackendTemporarilyUnavailableException.php',
'OCP\\Calendar\\Exceptions\\CalendarException' => __DIR__ . '/../../..' . '/lib/public/Calendar/Exceptions/CalendarException.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',
'OCP\\Calendar\\ICalendarIsWritable' => __DIR__ . '/../../..' . '/lib/public/Calendar/ICalendarIsWritable.php',
'OCP\\Calendar\\ICalendarProvider' => __DIR__ . '/../../..' . '/lib/public/Calendar/ICalendarProvider.php',
Expand Down Expand Up @@ -1157,6 +1158,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\\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',
'OC\\Calendar\\Resource\\Manager' => __DIR__ . '/../../..' . '/lib/private/Calendar/Resource/Manager.php',
Expand Down
132 changes: 132 additions & 0 deletions lib/private/Calendar/CalendarEventBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<?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 DateTimeInterface;
use InvalidArgumentException;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Calendar\ICalendarEventBuilder;
use OCP\Calendar\ICreateFromString;
use Sabre\VObject\Component\VCalendar;
use Sabre\VObject\Component\VEvent;

class CalendarEventBuilder implements ICalendarEventBuilder {
private ?DateTimeInterface $startDate = null;
private ?DateTimeInterface $endDate = null;
private ?string $summary = null;
private ?string $description = null;
private ?string $location = null;
private ?array $organizer = null;
private array $attendees = [];

public function __construct(
private readonly string $uid,
private readonly ITimeFactory $timeFactory,
) {
}

public function setStartDate(DateTimeInterface $start): ICalendarEventBuilder {
$this->startDate = $start;
return $this;
}

public function setEndDate(DateTimeInterface $end): ICalendarEventBuilder {
$this->endDate = $end;
return $this;
}

public function setSummary(string $summary): ICalendarEventBuilder {
$this->summary = $summary;
return $this;
}

public function setDescription(string $description): ICalendarEventBuilder {
$this->description = $description;
return $this;
}

public function setLocation(string $location): ICalendarEventBuilder {
$this->location = $location;
return $this;
}

public function setOrganizer(string $email, ?string $commonName = null): ICalendarEventBuilder {
$this->organizer = [$email, $commonName];
return $this;
}

public function addAttendee(string $email, ?string $commonName = null): ICalendarEventBuilder {
$this->attendees[] = [$email, $commonName];
return $this;
}

public function toIcs(): string {
if ($this->startDate === null) {
throw new InvalidArgumentException('Event is missing a start date');
}

if ($this->endDate === null) {
throw new InvalidArgumentException('Event is missing an end date');
}

if ($this->summary === null) {
throw new InvalidArgumentException('Event is missing a summary');
}

if ($this->organizer === null && $this->attendees !== []) {
throw new InvalidArgumentException('Event has attendees but is missing an organizer');
}

$vcalendar = new VCalendar();
$props = [
'UID' => $this->uid,
'DTSTAMP' => $this->timeFactory->now(),
'SUMMARY' => $this->summary,
'DTSTART' => $this->startDate,
'DTEND' => $this->endDate,
];
if ($this->description !== null) {
$props['DESCRIPTION'] = $this->description;
}
if ($this->location !== null) {
$props['LOCATION'] = $this->location;
}
/** @var VEvent $vevent */
$vevent = $vcalendar->add('VEVENT', $props);
if ($this->organizer !== null) {
self::addAttendeeToVEvent($vevent, 'ORGANIZER', $this->organizer);
}
foreach ($this->attendees as $attendee) {
self::addAttendeeToVEvent($vevent, 'ATTENDEE', $attendee);
}
return $vcalendar->serialize();
}

public function createInCalendar(ICreateFromString $calendar): string {
$fileName = $this->uid . '.ics';
$calendar->createFromString($fileName, $this->toIcs());
return $fileName;
}

/**
* @param array{0: string, 1: ?string} $tuple A tuple of [$email, $commonName] where $commonName may be null.
*/
private static function addAttendeeToVEvent(VEvent $vevent, string $name, array $tuple): void {
[$email, $cn] = $tuple;
if (!str_starts_with($email, 'mailto:')) {
$email = "mailto:$email";
}
$params = [];
if ($cn !== null) {
$params['CN'] = $cn;
}
$vevent->add($name, $email, $params);
}
}
30 changes: 19 additions & 11 deletions lib/private/Calendar/Manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Calendar\Exceptions\CalendarException;
use OCP\Calendar\ICalendar;
use OCP\Calendar\ICalendarEventBuilder;
use OCP\Calendar\ICalendarIsShared;
use OCP\Calendar\ICalendarIsWritable;
use OCP\Calendar\ICalendarProvider;
use OCP\Calendar\ICalendarQuery;
use OCP\Calendar\ICreateFromString;
use OCP\Calendar\IHandleImipMessage;
use OCP\Calendar\IManager;
use OCP\Security\ISecureRandom;
use Psr\Container\ContainerInterface;
use Psr\Log\LoggerInterface;
use Sabre\VObject\Component\VCalendar;
Expand All @@ -45,6 +47,7 @@ public function __construct(
private ContainerInterface $container,
private LoggerInterface $logger,
private ITimeFactory $timeFactory,
private ISecureRandom $random,
) {
}

Expand Down Expand Up @@ -216,21 +219,21 @@ public function handleIMipRequest(
string $recipient,
string $calendarData,
): bool {

$userCalendars = $this->getCalendarsForPrincipal($principalUri);
if (empty($userCalendars)) {
$this->logger->warning('iMip message could not be processed because user has no calendars');
return false;
}

/** @var VCalendar $vObject|null */
$calendarObject = Reader::read($calendarData);

if (!isset($calendarObject->METHOD) || $calendarObject->METHOD->getValue() !== 'REQUEST') {
$this->logger->warning('iMip message contains an incorrect or invalid method');
return false;
}

if (!isset($calendarObject->VEVENT)) {
$this->logger->warning('iMip message contains no event');
return false;
Expand All @@ -242,12 +245,12 @@ public function handleIMipRequest(
$this->logger->warning('iMip message event dose not contains a UID');
return false;
}

if (!isset($eventObject->ATTENDEE)) {
$this->logger->warning('iMip message event dose not contains any attendees');
return false;
}

foreach ($eventObject->ATTENDEE as $entry) {
$address = trim(str_replace('mailto:', '', $entry->getValue()));
if ($address === $recipient) {
Expand All @@ -259,17 +262,17 @@ public function handleIMipRequest(
$this->logger->warning('iMip message event does not contain a attendee that matches the recipient');
return false;
}

foreach ($userCalendars as $calendar) {

if (!$calendar instanceof ICalendarIsWritable && !$calendar instanceof ICalendarIsShared) {
continue;
}

if ($calendar->isDeleted() || !$calendar->isWritable() || $calendar->isShared()) {
continue;
}

if (!empty($calendar->search($recipient, ['ATTENDEE'], ['uid' => $eventObject->UID->getValue()]))) {
try {
if ($calendar instanceof IHandleImipMessage) {
Expand All @@ -282,7 +285,7 @@ public function handleIMipRequest(
}
}
}

$this->logger->warning('iMip message event could not be processed because the no corresponding event was found in any calendar');
return false;
}
Expand Down Expand Up @@ -464,4 +467,9 @@ public function handleIMipCancel(
return false;
}
}

public function createEventBuilder(): ICalendarEventBuilder {
$uid = $this->random->generate(32, ISecureRandom::CHAR_ALPHANUMERIC);
return new CalendarEventBuilder($uid, $this->timeFactory);
}
}
110 changes: 110 additions & 0 deletions lib/public/Calendar/ICalendarEventBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
<?php

declare(strict_types=1);

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

namespace OCP\Calendar;

use DateTimeInterface;
use InvalidArgumentException;
use OCP\Calendar\Exceptions\CalendarException;

/**
* The calendar event builder can be used to conveniently build a calendar event and then serialize
* it to a ICS string. The ICS string can be submitted to calendar instances implementing the
* {@see \OCP\Calendar\ICreateFromString} interface.
*
* Also note this class can not be injected directly with dependency injection.
* Instead, inject {@see \OCP\Calendar\IManager} and use
* {@see \OCP\Calendar\IManager::createEventBuilder()} afterwards.
*
* All setters return self to allow chaining method calls.
*
* @since 31.0.0
*/
interface ICalendarEventBuilder {
/**
* Set the start date, time and time zone.
* This property is required!
*
* @since 31.0.0
*/
public function setStartDate(DateTimeInterface $start): self;

/**
* Set the end date, time and time zone.
* This property is required!
*
* @since 31.0.0
*/
public function setEndDate(DateTimeInterface $end): self;

/**
* Set the event summary or title.
* This property is required!
*
* @since 31.0.0
*/
public function setSummary(string $summary): self;

/**
* Set the event description.
*
* @since 31.0.0
*/
public function setDescription(string $description): self;

/**
* Set the event location. It can either be a physical address or a URL.
*
* @since 31.0.0
*/
public function setLocation(string $location): self;

/**
* Set the event organizer.
* This property is required if attendees are added!
*
* The "mailto:" prefix is optional and will be added automatically if it is missing.
*
* @since 31.0.0
*/
public function setOrganizer(string $email, ?string $commonName = null): self;

/**
* Add a new attendee to the event.
* Adding at least one attendee requires also setting the organizer!
*
* The "mailto:" prefix is optional and will be added automatically if it is missing.
*
* @since 31.0.0
*/
public function addAttendee(string $email, ?string $commonName = null): self;

/**
* Serialize the built event to an ICS string if all required properties set.
*
* @since 31.0.0
*
* @return string The serialized ICS string
*
* @throws InvalidArgumentException If required properties were not set
*/
public function toIcs(): string;

/**
* Create the event in the given calendar.
*
* @since 31.0.0
*
* @return string The filename of the created event
*
* @throws InvalidArgumentException If required properties were not set
* @throws CalendarException If writing the event to the calendar fails
*/
public function createInCalendar(ICreateFromString $calendar): string;
}
8 changes: 8 additions & 0 deletions lib/public/Calendar/IManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -157,4 +157,12 @@ public function handleIMipReply(string $principalUri, string $sender, string $re
* @since 25.0.0
*/
public function handleIMipCancel(string $principalUri, string $sender, ?string $replyTo, string $recipient, string $calendarData): bool;

/**
* Create a new event builder instance. Please have a look at its documentation and the
* \OCP\Calendar\ICreateFromString interface on how to use it.
*
* @since 31.0.0
*/
public function createEventBuilder(): ICalendarEventBuilder;
}
Loading

0 comments on commit dd0f7f0

Please sign in to comment.