Skip to content

Commit dd0f7f0

Browse files
Merge pull request #49888 from nextcloud/feat/ocp/meetings-api-requirements
feat(ocp): calendar event builder api
2 parents e7122a6 + 42fa3ab commit dd0f7f0

12 files changed

+489
-27
lines changed

lib/composer/composer/autoload_classmap.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@
192192
'OCP\\Calendar\\BackendTemporarilyUnavailableException' => $baseDir . '/lib/public/Calendar/BackendTemporarilyUnavailableException.php',
193193
'OCP\\Calendar\\Exceptions\\CalendarException' => $baseDir . '/lib/public/Calendar/Exceptions/CalendarException.php',
194194
'OCP\\Calendar\\ICalendar' => $baseDir . '/lib/public/Calendar/ICalendar.php',
195+
'OCP\\Calendar\\ICalendarEventBuilder' => $baseDir . '/lib/public/Calendar/ICalendarEventBuilder.php',
195196
'OCP\\Calendar\\ICalendarIsShared' => $baseDir . '/lib/public/Calendar/ICalendarIsShared.php',
196197
'OCP\\Calendar\\ICalendarIsWritable' => $baseDir . '/lib/public/Calendar/ICalendarIsWritable.php',
197198
'OCP\\Calendar\\ICalendarProvider' => $baseDir . '/lib/public/Calendar/ICalendarProvider.php',
@@ -1116,6 +1117,7 @@
11161117
'OC\\Broadcast\\Events\\BroadcastEvent' => $baseDir . '/lib/private/Broadcast/Events/BroadcastEvent.php',
11171118
'OC\\Cache\\CappedMemoryCache' => $baseDir . '/lib/private/Cache/CappedMemoryCache.php',
11181119
'OC\\Cache\\File' => $baseDir . '/lib/private/Cache/File.php',
1120+
'OC\\Calendar\\CalendarEventBuilder' => $baseDir . '/lib/private/Calendar/CalendarEventBuilder.php',
11191121
'OC\\Calendar\\CalendarQuery' => $baseDir . '/lib/private/Calendar/CalendarQuery.php',
11201122
'OC\\Calendar\\Manager' => $baseDir . '/lib/private/Calendar/Manager.php',
11211123
'OC\\Calendar\\Resource\\Manager' => $baseDir . '/lib/private/Calendar/Resource/Manager.php',

lib/composer/composer/autoload_static.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
233233
'OCP\\Calendar\\BackendTemporarilyUnavailableException' => __DIR__ . '/../../..' . '/lib/public/Calendar/BackendTemporarilyUnavailableException.php',
234234
'OCP\\Calendar\\Exceptions\\CalendarException' => __DIR__ . '/../../..' . '/lib/public/Calendar/Exceptions/CalendarException.php',
235235
'OCP\\Calendar\\ICalendar' => __DIR__ . '/../../..' . '/lib/public/Calendar/ICalendar.php',
236+
'OCP\\Calendar\\ICalendarEventBuilder' => __DIR__ . '/../../..' . '/lib/public/Calendar/ICalendarEventBuilder.php',
236237
'OCP\\Calendar\\ICalendarIsShared' => __DIR__ . '/../../..' . '/lib/public/Calendar/ICalendarIsShared.php',
237238
'OCP\\Calendar\\ICalendarIsWritable' => __DIR__ . '/../../..' . '/lib/public/Calendar/ICalendarIsWritable.php',
238239
'OCP\\Calendar\\ICalendarProvider' => __DIR__ . '/../../..' . '/lib/public/Calendar/ICalendarProvider.php',
@@ -1157,6 +1158,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2
11571158
'OC\\Broadcast\\Events\\BroadcastEvent' => __DIR__ . '/../../..' . '/lib/private/Broadcast/Events/BroadcastEvent.php',
11581159
'OC\\Cache\\CappedMemoryCache' => __DIR__ . '/../../..' . '/lib/private/Cache/CappedMemoryCache.php',
11591160
'OC\\Cache\\File' => __DIR__ . '/../../..' . '/lib/private/Cache/File.php',
1161+
'OC\\Calendar\\CalendarEventBuilder' => __DIR__ . '/../../..' . '/lib/private/Calendar/CalendarEventBuilder.php',
11601162
'OC\\Calendar\\CalendarQuery' => __DIR__ . '/../../..' . '/lib/private/Calendar/CalendarQuery.php',
11611163
'OC\\Calendar\\Manager' => __DIR__ . '/../../..' . '/lib/private/Calendar/Manager.php',
11621164
'OC\\Calendar\\Resource\\Manager' => __DIR__ . '/../../..' . '/lib/private/Calendar/Resource/Manager.php',
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OC\Calendar;
11+
12+
use DateTimeInterface;
13+
use InvalidArgumentException;
14+
use OCP\AppFramework\Utility\ITimeFactory;
15+
use OCP\Calendar\ICalendarEventBuilder;
16+
use OCP\Calendar\ICreateFromString;
17+
use Sabre\VObject\Component\VCalendar;
18+
use Sabre\VObject\Component\VEvent;
19+
20+
class CalendarEventBuilder implements ICalendarEventBuilder {
21+
private ?DateTimeInterface $startDate = null;
22+
private ?DateTimeInterface $endDate = null;
23+
private ?string $summary = null;
24+
private ?string $description = null;
25+
private ?string $location = null;
26+
private ?array $organizer = null;
27+
private array $attendees = [];
28+
29+
public function __construct(
30+
private readonly string $uid,
31+
private readonly ITimeFactory $timeFactory,
32+
) {
33+
}
34+
35+
public function setStartDate(DateTimeInterface $start): ICalendarEventBuilder {
36+
$this->startDate = $start;
37+
return $this;
38+
}
39+
40+
public function setEndDate(DateTimeInterface $end): ICalendarEventBuilder {
41+
$this->endDate = $end;
42+
return $this;
43+
}
44+
45+
public function setSummary(string $summary): ICalendarEventBuilder {
46+
$this->summary = $summary;
47+
return $this;
48+
}
49+
50+
public function setDescription(string $description): ICalendarEventBuilder {
51+
$this->description = $description;
52+
return $this;
53+
}
54+
55+
public function setLocation(string $location): ICalendarEventBuilder {
56+
$this->location = $location;
57+
return $this;
58+
}
59+
60+
public function setOrganizer(string $email, ?string $commonName = null): ICalendarEventBuilder {
61+
$this->organizer = [$email, $commonName];
62+
return $this;
63+
}
64+
65+
public function addAttendee(string $email, ?string $commonName = null): ICalendarEventBuilder {
66+
$this->attendees[] = [$email, $commonName];
67+
return $this;
68+
}
69+
70+
public function toIcs(): string {
71+
if ($this->startDate === null) {
72+
throw new InvalidArgumentException('Event is missing a start date');
73+
}
74+
75+
if ($this->endDate === null) {
76+
throw new InvalidArgumentException('Event is missing an end date');
77+
}
78+
79+
if ($this->summary === null) {
80+
throw new InvalidArgumentException('Event is missing a summary');
81+
}
82+
83+
if ($this->organizer === null && $this->attendees !== []) {
84+
throw new InvalidArgumentException('Event has attendees but is missing an organizer');
85+
}
86+
87+
$vcalendar = new VCalendar();
88+
$props = [
89+
'UID' => $this->uid,
90+
'DTSTAMP' => $this->timeFactory->now(),
91+
'SUMMARY' => $this->summary,
92+
'DTSTART' => $this->startDate,
93+
'DTEND' => $this->endDate,
94+
];
95+
if ($this->description !== null) {
96+
$props['DESCRIPTION'] = $this->description;
97+
}
98+
if ($this->location !== null) {
99+
$props['LOCATION'] = $this->location;
100+
}
101+
/** @var VEvent $vevent */
102+
$vevent = $vcalendar->add('VEVENT', $props);
103+
if ($this->organizer !== null) {
104+
self::addAttendeeToVEvent($vevent, 'ORGANIZER', $this->organizer);
105+
}
106+
foreach ($this->attendees as $attendee) {
107+
self::addAttendeeToVEvent($vevent, 'ATTENDEE', $attendee);
108+
}
109+
return $vcalendar->serialize();
110+
}
111+
112+
public function createInCalendar(ICreateFromString $calendar): string {
113+
$fileName = $this->uid . '.ics';
114+
$calendar->createFromString($fileName, $this->toIcs());
115+
return $fileName;
116+
}
117+
118+
/**
119+
* @param array{0: string, 1: ?string} $tuple A tuple of [$email, $commonName] where $commonName may be null.
120+
*/
121+
private static function addAttendeeToVEvent(VEvent $vevent, string $name, array $tuple): void {
122+
[$email, $cn] = $tuple;
123+
if (!str_starts_with($email, 'mailto:')) {
124+
$email = "mailto:$email";
125+
}
126+
$params = [];
127+
if ($cn !== null) {
128+
$params['CN'] = $cn;
129+
}
130+
$vevent->add($name, $email, $params);
131+
}
132+
}

lib/private/Calendar/Manager.php

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,15 @@
1212
use OCP\AppFramework\Utility\ITimeFactory;
1313
use OCP\Calendar\Exceptions\CalendarException;
1414
use OCP\Calendar\ICalendar;
15+
use OCP\Calendar\ICalendarEventBuilder;
1516
use OCP\Calendar\ICalendarIsShared;
1617
use OCP\Calendar\ICalendarIsWritable;
1718
use OCP\Calendar\ICalendarProvider;
1819
use OCP\Calendar\ICalendarQuery;
1920
use OCP\Calendar\ICreateFromString;
2021
use OCP\Calendar\IHandleImipMessage;
2122
use OCP\Calendar\IManager;
23+
use OCP\Security\ISecureRandom;
2224
use Psr\Container\ContainerInterface;
2325
use Psr\Log\LoggerInterface;
2426
use Sabre\VObject\Component\VCalendar;
@@ -45,6 +47,7 @@ public function __construct(
4547
private ContainerInterface $container,
4648
private LoggerInterface $logger,
4749
private ITimeFactory $timeFactory,
50+
private ISecureRandom $random,
4851
) {
4952
}
5053

@@ -216,21 +219,21 @@ public function handleIMipRequest(
216219
string $recipient,
217220
string $calendarData,
218221
): bool {
219-
222+
220223
$userCalendars = $this->getCalendarsForPrincipal($principalUri);
221224
if (empty($userCalendars)) {
222225
$this->logger->warning('iMip message could not be processed because user has no calendars');
223226
return false;
224227
}
225-
228+
226229
/** @var VCalendar $vObject|null */
227230
$calendarObject = Reader::read($calendarData);
228-
231+
229232
if (!isset($calendarObject->METHOD) || $calendarObject->METHOD->getValue() !== 'REQUEST') {
230233
$this->logger->warning('iMip message contains an incorrect or invalid method');
231234
return false;
232235
}
233-
236+
234237
if (!isset($calendarObject->VEVENT)) {
235238
$this->logger->warning('iMip message contains no event');
236239
return false;
@@ -242,12 +245,12 @@ public function handleIMipRequest(
242245
$this->logger->warning('iMip message event dose not contains a UID');
243246
return false;
244247
}
245-
248+
246249
if (!isset($eventObject->ATTENDEE)) {
247250
$this->logger->warning('iMip message event dose not contains any attendees');
248251
return false;
249252
}
250-
253+
251254
foreach ($eventObject->ATTENDEE as $entry) {
252255
$address = trim(str_replace('mailto:', '', $entry->getValue()));
253256
if ($address === $recipient) {
@@ -259,17 +262,17 @@ public function handleIMipRequest(
259262
$this->logger->warning('iMip message event does not contain a attendee that matches the recipient');
260263
return false;
261264
}
262-
265+
263266
foreach ($userCalendars as $calendar) {
264-
267+
265268
if (!$calendar instanceof ICalendarIsWritable && !$calendar instanceof ICalendarIsShared) {
266269
continue;
267270
}
268-
271+
269272
if ($calendar->isDeleted() || !$calendar->isWritable() || $calendar->isShared()) {
270273
continue;
271274
}
272-
275+
273276
if (!empty($calendar->search($recipient, ['ATTENDEE'], ['uid' => $eventObject->UID->getValue()]))) {
274277
try {
275278
if ($calendar instanceof IHandleImipMessage) {
@@ -282,7 +285,7 @@ public function handleIMipRequest(
282285
}
283286
}
284287
}
285-
288+
286289
$this->logger->warning('iMip message event could not be processed because the no corresponding event was found in any calendar');
287290
return false;
288291
}
@@ -464,4 +467,9 @@ public function handleIMipCancel(
464467
return false;
465468
}
466469
}
470+
471+
public function createEventBuilder(): ICalendarEventBuilder {
472+
$uid = $this->random->generate(32, ISecureRandom::CHAR_ALPHANUMERIC);
473+
return new CalendarEventBuilder($uid, $this->timeFactory);
474+
}
467475
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCP\Calendar;
11+
12+
use DateTimeInterface;
13+
use InvalidArgumentException;
14+
use OCP\Calendar\Exceptions\CalendarException;
15+
16+
/**
17+
* The calendar event builder can be used to conveniently build a calendar event and then serialize
18+
* it to a ICS string. The ICS string can be submitted to calendar instances implementing the
19+
* {@see \OCP\Calendar\ICreateFromString} interface.
20+
*
21+
* Also note this class can not be injected directly with dependency injection.
22+
* Instead, inject {@see \OCP\Calendar\IManager} and use
23+
* {@see \OCP\Calendar\IManager::createEventBuilder()} afterwards.
24+
*
25+
* All setters return self to allow chaining method calls.
26+
*
27+
* @since 31.0.0
28+
*/
29+
interface ICalendarEventBuilder {
30+
/**
31+
* Set the start date, time and time zone.
32+
* This property is required!
33+
*
34+
* @since 31.0.0
35+
*/
36+
public function setStartDate(DateTimeInterface $start): self;
37+
38+
/**
39+
* Set the end date, time and time zone.
40+
* This property is required!
41+
*
42+
* @since 31.0.0
43+
*/
44+
public function setEndDate(DateTimeInterface $end): self;
45+
46+
/**
47+
* Set the event summary or title.
48+
* This property is required!
49+
*
50+
* @since 31.0.0
51+
*/
52+
public function setSummary(string $summary): self;
53+
54+
/**
55+
* Set the event description.
56+
*
57+
* @since 31.0.0
58+
*/
59+
public function setDescription(string $description): self;
60+
61+
/**
62+
* Set the event location. It can either be a physical address or a URL.
63+
*
64+
* @since 31.0.0
65+
*/
66+
public function setLocation(string $location): self;
67+
68+
/**
69+
* Set the event organizer.
70+
* This property is required if attendees are added!
71+
*
72+
* The "mailto:" prefix is optional and will be added automatically if it is missing.
73+
*
74+
* @since 31.0.0
75+
*/
76+
public function setOrganizer(string $email, ?string $commonName = null): self;
77+
78+
/**
79+
* Add a new attendee to the event.
80+
* Adding at least one attendee requires also setting the organizer!
81+
*
82+
* The "mailto:" prefix is optional and will be added automatically if it is missing.
83+
*
84+
* @since 31.0.0
85+
*/
86+
public function addAttendee(string $email, ?string $commonName = null): self;
87+
88+
/**
89+
* Serialize the built event to an ICS string if all required properties set.
90+
*
91+
* @since 31.0.0
92+
*
93+
* @return string The serialized ICS string
94+
*
95+
* @throws InvalidArgumentException If required properties were not set
96+
*/
97+
public function toIcs(): string;
98+
99+
/**
100+
* Create the event in the given calendar.
101+
*
102+
* @since 31.0.0
103+
*
104+
* @return string The filename of the created event
105+
*
106+
* @throws InvalidArgumentException If required properties were not set
107+
* @throws CalendarException If writing the event to the calendar fails
108+
*/
109+
public function createInCalendar(ICreateFromString $calendar): string;
110+
}

lib/public/Calendar/IManager.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,4 +157,12 @@ public function handleIMipReply(string $principalUri, string $sender, string $re
157157
* @since 25.0.0
158158
*/
159159
public function handleIMipCancel(string $principalUri, string $sender, ?string $replyTo, string $recipient, string $calendarData): bool;
160+
161+
/**
162+
* Create a new event builder instance. Please have a look at its documentation and the
163+
* \OCP\Calendar\ICreateFromString interface on how to use it.
164+
*
165+
* @since 31.0.0
166+
*/
167+
public function createEventBuilder(): ICalendarEventBuilder;
160168
}

0 commit comments

Comments
 (0)