diff options
author | Anna Larch <anna@nextcloud.com> | 2023-09-25 14:47:02 +0200 |
---|---|---|
committer | Anna Larch <anna@nextcloud.com> | 2023-11-09 16:20:19 +0100 |
commit | f14a4f8fd73c71e76a9747ac51e657030f5bb835 (patch) | |
tree | 6d5ec8e5365a72b83bfd270ce56bcbaa06703b8a /apps/dav/lib/CalDAV | |
parent | 1aa24c024e207b54df3867f5f7ccd67625ac0492 (diff) | |
download | nextcloud-server-f14a4f8fd73c71e76a9747ac51e657030f5bb835.tar.gz nextcloud-server-f14a4f8fd73c71e76a9747ac51e657030f5bb835.zip |
feat(user status): automate user status for events
and automatically set a user status to free or busy depending on their calendar
transparency, event status and availability settings
Signed-off-by: Anna Larch <anna@nextcloud.com>
Diffstat (limited to 'apps/dav/lib/CalDAV')
-rw-r--r-- | apps/dav/lib/CalDAV/CalendarImpl.php | 27 | ||||
-rw-r--r-- | apps/dav/lib/CalDAV/FreeBusy/FreeBusyGenerator.php | 44 | ||||
-rw-r--r-- | apps/dav/lib/CalDAV/Schedule/Plugin.php | 2 | ||||
-rw-r--r-- | apps/dav/lib/CalDAV/Status/Status.php | 57 | ||||
-rw-r--r-- | apps/dav/lib/CalDAV/Status/StatusService.php | 236 |
5 files changed, 366 insertions, 0 deletions
diff --git a/apps/dav/lib/CalDAV/CalendarImpl.php b/apps/dav/lib/CalDAV/CalendarImpl.php index cc57aa36469..de20c9ac3ae 100644 --- a/apps/dav/lib/CalDAV/CalendarImpl.php +++ b/apps/dav/lib/CalDAV/CalendarImpl.php @@ -33,11 +33,15 @@ use OCA\DAV\CalDAV\InvitationResponse\InvitationResponseServer; use OCP\Calendar\Exceptions\CalendarException; use OCP\Calendar\ICreateFromString; use OCP\Calendar\IHandleImipMessage; +use OCP\Calendar\ISchedulingInformation; use OCP\Constants; +use Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp; use Sabre\DAV\Exception\Conflict; use Sabre\VObject\Component\VCalendar; use Sabre\VObject\Component\VEvent; +use Sabre\VObject\Component\VTimeZone; use Sabre\VObject\ITip\Message; +use Sabre\VObject\Property; use Sabre\VObject\Reader; use function Sabre\Uri\split as uriSplit; @@ -86,6 +90,29 @@ class CalendarImpl implements ICreateFromString, IHandleImipMessage { return $this->calendarInfo['{http://apple.com/ns/ical/}calendar-color']; } + public function getSchedulingTransparency(): ?ScheduleCalendarTransp { + return $this->calendarInfo['{' . \OCA\DAV\CalDAV\Schedule\Plugin::NS_CALDAV . '}schedule-calendar-transp']; + } + + public function getSchedulingTimezone(): ?VTimeZone { + $tzProp = '{' . \OCA\DAV\CalDAV\Schedule\Plugin::NS_CALDAV . '}calendar-timezone'; + if (!isset($this->calendarInfo[$tzProp])) { + return null; + } + // This property contains a VCALENDAR with a single VTIMEZONE + /** @var string $timezoneProp */ + $timezoneProp = $this->calendarInfo[$tzProp]; + /** @var VCalendar $vobj */ + $vobj = Reader::read($timezoneProp); + $components = $vobj->getComponents(); + if(empty($components)) { + return null; + } + /** @var VTimeZone $vtimezone */ + $vtimezone = $components[0]; + return $vtimezone; + } + /** * @param string $pattern which should match within the $searchProperties * @param array $searchProperties defines the properties within the query pattern should match diff --git a/apps/dav/lib/CalDAV/FreeBusy/FreeBusyGenerator.php b/apps/dav/lib/CalDAV/FreeBusy/FreeBusyGenerator.php new file mode 100644 index 00000000000..29daca4e092 --- /dev/null +++ b/apps/dav/lib/CalDAV/FreeBusy/FreeBusyGenerator.php @@ -0,0 +1,44 @@ +<?php +declare(strict_types=1); +/* + * * + * * + * * @copyright 2023 Anna Larch <anna.larch@gmx.net> + * * + * * @author Anna Larch <anna.larch@gmx.net> + * * + * * This library is free software; you can redistribute it and/or + * * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * * License as published by the Free Software Foundation; either + * * version 3 of the License, or any later version. + * * + * * This library is distributed in the hope that it will be useful, + * * but WITHOUT ANY WARRANTY; without even the implied warranty of + * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * * GNU AFFERO GENERAL PUBLIC LICENSE for more details. + * * + * * You should have received a copy of the GNU Affero General Public + * * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * * + * + */ + +namespace OCA\DAV\CalDAV\FreeBusy; + +use DateTimeInterface; +use DateTimeZone; +use Sabre\VObject\Component\VCalendar; + +/** + * @psalm-suppress PropertyNotSetInConstructor + */ +class FreeBusyGenerator extends \Sabre\VObject\FreeBusyGenerator { + + public function __construct() { + parent::__construct(); + } + + public function getVCalendar(): VCalendar { + return new VCalendar(); + } +} diff --git a/apps/dav/lib/CalDAV/Schedule/Plugin.php b/apps/dav/lib/CalDAV/Schedule/Plugin.php index 2845ccdf6c2..16acc72d988 100644 --- a/apps/dav/lib/CalDAV/Schedule/Plugin.php +++ b/apps/dav/lib/CalDAV/Schedule/Plugin.php @@ -36,6 +36,7 @@ use OCA\DAV\CalDAV\CalendarHome; use OCP\IConfig; use Psr\Log\LoggerInterface; use Sabre\CalDAV\ICalendar; +use Sabre\CalDAV\Schedule\IOutbox; use Sabre\DAV\INode; use Sabre\DAV\IProperties; use Sabre\DAV\PropFind; @@ -44,6 +45,7 @@ use Sabre\DAV\Xml\Property\LocalHref; use Sabre\DAVACL\IPrincipal; use Sabre\HTTP\RequestInterface; use Sabre\HTTP\ResponseInterface; +use Sabre\VObject; use Sabre\VObject\Component; use Sabre\VObject\Component\VCalendar; use Sabre\VObject\Component\VEvent; diff --git a/apps/dav/lib/CalDAV/Status/Status.php b/apps/dav/lib/CalDAV/Status/Status.php new file mode 100644 index 00000000000..8857d0f14e7 --- /dev/null +++ b/apps/dav/lib/CalDAV/Status/Status.php @@ -0,0 +1,57 @@ +<?php +/* + * * + * * Dav App + * * + * * @copyright 2023 Anna Larch <anna.larch@gmx.net> + * * + * * @author Anna Larch <anna.larch@gmx.net> + * * + * * This library is free software; you can redistribute it and/or + * * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * * License as published by the Free Software Foundation; either + * * version 3 of the License, or any later version. + * * + * * This library is distributed in the hope that it will be useful, + * * but WITHOUT ANY WARRANTY; without even the implied warranty of + * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * * GNU AFFERO GENERAL PUBLIC LICENSE for more details. + * * + * * You should have received a copy of the GNU Affero General Public + * * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * * + * + */ + +namespace OCA\DAV\CalDAV\Status; + +class Status { + + public function __construct(private string $status = '', private ?string $message = null, private ?string $customMessage = null){} + + public function getStatus(): string { + return $this->status; + } + + public function setStatus(string $status): void { + $this->status = $status; + } + + public function getMessage(): ?string { + return $this->message; + } + + public function setMessage(?string $message): void { + $this->message = $message; + } + + public function getCustomMessage(): ?string { + return $this->customMessage; + } + + public function setCustomMessage(?string $customMessage): void { + $this->customMessage = $customMessage; + } + + +} diff --git a/apps/dav/lib/CalDAV/Status/StatusService.php b/apps/dav/lib/CalDAV/Status/StatusService.php new file mode 100644 index 00000000000..92554f800c3 --- /dev/null +++ b/apps/dav/lib/CalDAV/Status/StatusService.php @@ -0,0 +1,236 @@ +<?php +/* + * * + * * Dav App + * * + * * @copyright 2023 Anna Larch <anna.larch@gmx.net> + * * + * * @author Anna Larch <anna.larch@gmx.net> + * * + * * This library is free software; you can redistribute it and/or + * * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE + * * License as published by the Free Software Foundation; either + * * version 3 of the License, or any later version. + * * + * * This library is distributed in the hope that it will be useful, + * * but WITHOUT ANY WARRANTY; without even the implied warranty of + * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * * GNU AFFERO GENERAL PUBLIC LICENSE for more details. + * * + * * You should have received a copy of the GNU Affero General Public + * * License along with this library. If not, see <http://www.gnu.org/licenses/>. + * * + * + */ + +declare(strict_types=1); + +/** + * @copyright 2023 Anna Larch <anna.larch@gmx.net> + * + * @author Anna Larch <anna.larch@gmx.net> + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ +namespace OCA\DAV\CalDAV\Status; + +use DateTimeZone; +use OC\Calendar\CalendarQuery; +use OCA\DAV\CalDAV\CalendarImpl; +use OCA\DAV\CalDAV\FreeBusy\FreeBusyGenerator; +use OCA\DAV\CalDAV\InvitationResponse\InvitationResponseServer; +use OCA\DAV\CalDAV\IUser; +use OCA\DAV\CalDAV\Schedule\Plugin as SchedulePlugin; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\Calendar\IManager; +use OCP\Calendar\ISchedulingInformation; +use OCP\IL10N; +use OCP\IUser as User; +use OCP\UserStatus\IUserStatus; +use Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp; +use Sabre\DAV\Exception\NotAuthenticated; +use Sabre\DAVACL\Exception\NeedPrivileges; +use Sabre\DAVACL\Plugin as AclPlugin; +use Sabre\VObject\Component; +use Sabre\VObject\Component\VCalendar; +use Sabre\VObject\Component\VEvent; +use Sabre\VObject\Parameter; +use Sabre\VObject\Property; +use Sabre\VObject\Reader; + +class StatusService { + public function __construct(private ITimeFactory $timeFactory, + private IManager $calendarManager, + private InvitationResponseServer $server, + private IL10N $l10n, + private FreeBusyGenerator $generator){} + + public function processCalendarAvailability(User $user, ?string $availability): ?Status { + $userId = $user->getUID(); + $email = $user->getEMailAddress(); + if($email === null) { + return null; + } + + $server = $this->server->getServer(); + + /** @var SchedulePlugin $schedulingPlugin */ + $schedulingPlugin = $server->getPlugin('caldav-schedule'); + $caldavNS = '{'.$schedulingPlugin::NS_CALDAV.'}'; + + /** @var AclPlugin $aclPlugin */ + $aclPlugin = $server->getPlugin('acl'); + if ('mailto:' === substr($email, 0, 7)) { + $email = substr($email, 7); + } + + $result = $aclPlugin->principalSearch( + ['{http://sabredav.org/ns}email-address' => $email], + [ + '{DAV:}principal-URL', + $caldavNS.'calendar-home-set', + $caldavNS.'schedule-inbox-URL', + '{http://sabredav.org/ns}email-address', + ] + ); + + if (!count($result) || !isset($result[0][200][$caldavNS.'schedule-inbox-URL'])) { + return null; + } + + $inboxUrl = $result[0][200][$caldavNS.'schedule-inbox-URL']->getHref(); + + // Do we have permission? + try { + $aclPlugin->checkPrivileges($inboxUrl, $caldavNS.'schedule-query-freebusy'); + } catch (NeedPrivileges | NotAuthenticated $exception) { + return null; + } + + $now = $this->timeFactory->now(); + $calendarTimeZone = $now->getTimezone(); + $calendars = $this->calendarManager->getCalendarsForPrincipal('principals/users/' . $userId); + if(empty($calendars)) { + return null; + } + + $query = $this->calendarManager->newQuery('principals/users/' . $userId); + foreach ($calendars as $calendarObject) { + // We can only work with a calendar if it exposes its scheduling information + if (!$calendarObject instanceof CalendarImpl) { + continue; + } + + $sct = $calendarObject->getSchedulingTransparency(); + if ($sct !== null && ScheduleCalendarTransp::TRANSPARENT == strtolower($sct->getValue())) { + // If a calendar is marked as 'transparent', it means we must + // ignore it for free-busy purposes. + continue; + } + + /** @var Component\VTimeZone|null $ctz */ + $ctz = $calendarObject->getSchedulingTimezone(); + if ($ctz !== null) { + $calendarTimeZone = $ctz->getTimeZone(); + } + $query->addSearchCalendar($calendarObject->getUri()); + } + + $calendarEvents = []; + $dtStart = $now; + $dtEnd = \DateTimeImmutable::createFromMutable($this->timeFactory->getDateTime('+10 minutes')); + + // Only query the calendars when there's any to search + if($query instanceof CalendarQuery && !empty($query->getCalendarUris())) { + // Query the next hour + $query->setTimerangeStart($dtStart); + $query->setTimerangeEnd($dtEnd); + $calendarEvents = $this->calendarManager->searchForPrincipal($query); + } + + // @todo we can cache that + if(empty($availability) && empty($calendarEvents)) { + // No availability settings and no calendar events, we can stop here + return null; + } + + $calendar = $this->generator->getVCalendar(); + foreach ($calendarEvents as $calendarEvent) { + $vEvent = new VEvent($calendar, 'VEVENT'); + foreach($calendarEvent['objects'] as $component) { + foreach ($component as $key => $value) { + $vEvent->add($key, $value[0]); + } + } + $calendar->add($vEvent); + } + + $calendar->METHOD = 'REQUEST'; + + $this->generator->setObjects($calendar); + $this->generator->setTimeRange($dtStart, $dtEnd); + $this->generator->setTimeZone($calendarTimeZone); + + if (!empty($availability)) { + $this->generator->setVAvailability( + Reader::read( + $availability + ) + ); + } + // Generate the intersection of VAVILABILITY and all VEVENTS in all calendars + $result = $this->generator->getResult(); + + if (!isset($result->VFREEBUSY)) { + return null; + } + + /** @var Component $freeBusyComponent */ + $freeBusyComponent = $result->VFREEBUSY; + $freeBusyProperties = $freeBusyComponent->select('FREEBUSY'); + // If there is no FreeBusy property, the time-range is empty and available + // so set the status to online as otherwise we will never recover from a BUSY status + if (count($freeBusyProperties) === 0) { + return new Status(IUserStatus::ONLINE); + } + + /** @var Property $freeBusyProperty */ + $freeBusyProperty = $freeBusyProperties[0]; + if (!$freeBusyProperty->offsetExists('FBTYPE')) { + // If there is no FBTYPE, it means it's busy from a regular event + return new Status(IUserStatus::BUSY, IUserStatus::MESSAGE_CALENDAR_BUSY); + } + + // If we can't deal with the FBTYPE (custom properties are a possibility) + // we should ignore it and leave the current status + $fbTypeParameter = $freeBusyProperty->offsetGet('FBTYPE'); + if (!($fbTypeParameter instanceof Parameter)) { + return null; + } + $fbType = $fbTypeParameter->getValue(); + switch ($fbType) { + case 'BUSY': + return new Status(IUserStatus::BUSY, IUserStatus::MESSAGE_CALENDAR_BUSY, $this->l10n->t('In a meeting')); + case 'BUSY-UNAVAILABLE': + return new Status(IUserStatus::AWAY, IUserStatus::MESSAGE_AVAILABILITY); + case 'BUSY-TENTATIVE': + return new Status(IUserStatus::AWAY, IUserStatus::MESSAGE_CALENDAR_BUSY_TENTATIVE); + default: + return null; + } + } +} |