Merge pull request #40615 from nextcloud/enh/automate-calendar-event-user-status
feat(user status): automate user status for eventspull/41308/head
commit
7248d9b091
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue