Browse Source
feat(user status): automate user status for events
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>pull/40615/head
17 changed files with 2347 additions and 68 deletions
-
3apps/dav/composer/composer/autoload_classmap.php
-
3apps/dav/composer/composer/autoload_static.php
-
27apps/dav/lib/CalDAV/CalendarImpl.php
-
44apps/dav/lib/CalDAV/FreeBusy/FreeBusyGenerator.php
-
2apps/dav/lib/CalDAV/Schedule/Plugin.php
-
57apps/dav/lib/CalDAV/Status/Status.php
-
236apps/dav/lib/CalDAV/Status/StatusService.php
-
1508apps/dav/tests/unit/CalDAV/Status/StatusServiceTest.php
-
20apps/user_status/lib/Db/UserStatusMapper.php
-
2apps/user_status/lib/Listener/UserLiveStatusListener.php
-
2apps/user_status/lib/Service/PredefinedStatusService.php
-
140apps/user_status/lib/Service/StatusService.php
-
2apps/user_status/src/mixins/OnlineStatusMixin.js
-
345apps/user_status/tests/Unit/Service/StatusServiceTest.php
-
4dist/user_status-menu.js
-
2dist/user_status-menu.js.map
-
18lib/public/UserStatus/IUserStatus.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(); |
||||
|
} |
||||
|
} |
||||
@ -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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
1508
apps/dav/tests/unit/CalDAV/Status/StatusServiceTest.php
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
4
dist/user_status-menu.js
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
2
dist/user_status-menu.js.map
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
Write
Preview
Loading…
Cancel
Save
Reference in new issue