mirror of
https://github.com/nextcloud/server.git
synced 2026-06-09 00:32:29 -04:00
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>
This commit is contained in:
parent
1aa24c024e
commit
f14a4f8fd7
17 changed files with 2347 additions and 68 deletions
|
|
@ -55,6 +55,7 @@ return array(
|
|||
'OCA\\DAV\\CalDAV\\CalendarProvider' => $baseDir . '/../lib/CalDAV/CalendarProvider.php',
|
||||
'OCA\\DAV\\CalDAV\\CalendarRoot' => $baseDir . '/../lib/CalDAV/CalendarRoot.php',
|
||||
'OCA\\DAV\\CalDAV\\EventComparisonService' => $baseDir . '/../lib/CalDAV/EventComparisonService.php',
|
||||
'OCA\\DAV\\CalDAV\\FreeBusy\\FreeBusyGenerator' => $baseDir . '/../lib/CalDAV/FreeBusy/FreeBusyGenerator.php',
|
||||
'OCA\\DAV\\CalDAV\\ICSExportPlugin\\ICSExportPlugin' => $baseDir . '/../lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php',
|
||||
'OCA\\DAV\\CalDAV\\IRestorable' => $baseDir . '/../lib/CalDAV/IRestorable.php',
|
||||
'OCA\\DAV\\CalDAV\\Integration\\ExternalCalendar' => $baseDir . '/../lib/CalDAV/Integration/ExternalCalendar.php',
|
||||
|
|
@ -97,6 +98,8 @@ return array(
|
|||
'OCA\\DAV\\CalDAV\\Search\\Xml\\Filter\\PropFilter' => $baseDir . '/../lib/CalDAV/Search/Xml/Filter/PropFilter.php',
|
||||
'OCA\\DAV\\CalDAV\\Search\\Xml\\Filter\\SearchTermFilter' => $baseDir . '/../lib/CalDAV/Search/Xml/Filter/SearchTermFilter.php',
|
||||
'OCA\\DAV\\CalDAV\\Search\\Xml\\Request\\CalendarSearchReport' => $baseDir . '/../lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php',
|
||||
'OCA\\DAV\\CalDAV\\Status\\Status' => $baseDir . '/../lib/CalDAV/Status/Status.php',
|
||||
'OCA\\DAV\\CalDAV\\Status\\StatusService' => $baseDir . '/../lib/CalDAV/Status/StatusService.php',
|
||||
'OCA\\DAV\\CalDAV\\Trashbin\\DeletedCalendarObject' => $baseDir . '/../lib/CalDAV/Trashbin/DeletedCalendarObject.php',
|
||||
'OCA\\DAV\\CalDAV\\Trashbin\\DeletedCalendarObjectsCollection' => $baseDir . '/../lib/CalDAV/Trashbin/DeletedCalendarObjectsCollection.php',
|
||||
'OCA\\DAV\\CalDAV\\Trashbin\\Plugin' => $baseDir . '/../lib/CalDAV/Trashbin/Plugin.php',
|
||||
|
|
|
|||
|
|
@ -70,6 +70,7 @@ class ComposerStaticInitDAV
|
|||
'OCA\\DAV\\CalDAV\\CalendarProvider' => __DIR__ . '/..' . '/../lib/CalDAV/CalendarProvider.php',
|
||||
'OCA\\DAV\\CalDAV\\CalendarRoot' => __DIR__ . '/..' . '/../lib/CalDAV/CalendarRoot.php',
|
||||
'OCA\\DAV\\CalDAV\\EventComparisonService' => __DIR__ . '/..' . '/../lib/CalDAV/EventComparisonService.php',
|
||||
'OCA\\DAV\\CalDAV\\FreeBusy\\FreeBusyGenerator' => __DIR__ . '/..' . '/../lib/CalDAV/FreeBusy/FreeBusyGenerator.php',
|
||||
'OCA\\DAV\\CalDAV\\ICSExportPlugin\\ICSExportPlugin' => __DIR__ . '/..' . '/../lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php',
|
||||
'OCA\\DAV\\CalDAV\\IRestorable' => __DIR__ . '/..' . '/../lib/CalDAV/IRestorable.php',
|
||||
'OCA\\DAV\\CalDAV\\Integration\\ExternalCalendar' => __DIR__ . '/..' . '/../lib/CalDAV/Integration/ExternalCalendar.php',
|
||||
|
|
@ -112,6 +113,8 @@ class ComposerStaticInitDAV
|
|||
'OCA\\DAV\\CalDAV\\Search\\Xml\\Filter\\PropFilter' => __DIR__ . '/..' . '/../lib/CalDAV/Search/Xml/Filter/PropFilter.php',
|
||||
'OCA\\DAV\\CalDAV\\Search\\Xml\\Filter\\SearchTermFilter' => __DIR__ . '/..' . '/../lib/CalDAV/Search/Xml/Filter/SearchTermFilter.php',
|
||||
'OCA\\DAV\\CalDAV\\Search\\Xml\\Request\\CalendarSearchReport' => __DIR__ . '/..' . '/../lib/CalDAV/Search/Xml/Request/CalendarSearchReport.php',
|
||||
'OCA\\DAV\\CalDAV\\Status\\Status' => __DIR__ . '/..' . '/../lib/CalDAV/Status/Status.php',
|
||||
'OCA\\DAV\\CalDAV\\Status\\StatusService' => __DIR__ . '/..' . '/../lib/CalDAV/Status/StatusService.php',
|
||||
'OCA\\DAV\\CalDAV\\Trashbin\\DeletedCalendarObject' => __DIR__ . '/..' . '/../lib/CalDAV/Trashbin/DeletedCalendarObject.php',
|
||||
'OCA\\DAV\\CalDAV\\Trashbin\\DeletedCalendarObjectsCollection' => __DIR__ . '/..' . '/../lib/CalDAV/Trashbin/DeletedCalendarObjectsCollection.php',
|
||||
'OCA\\DAV\\CalDAV\\Trashbin\\Plugin' => __DIR__ . '/..' . '/../lib/CalDAV/Trashbin/Plugin.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
|
||||
|
|
|
|||
44
apps/dav/lib/CalDAV/FreeBusy/FreeBusyGenerator.php
Normal file
44
apps/dav/lib/CalDAV/FreeBusy/FreeBusyGenerator.php
Normal file
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
57
apps/dav/lib/CalDAV/Status/Status.php
Normal file
57
apps/dav/lib/CalDAV/Status/Status.php
Normal file
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
236
apps/dav/lib/CalDAV/Status/StatusService.php
Normal file
236
apps/dav/lib/CalDAV/Status/StatusService.php
Normal file
|
|
@ -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
Normal file
1508
apps/dav/tests/unit/CalDAV/Status/StatusServiceTest.php
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -26,6 +26,7 @@ declare(strict_types=1);
|
|||
|
||||
namespace OCA\UserStatus\Db;
|
||||
|
||||
use Sabre\CalDAV\Schedule\Plugin;
|
||||
use OCP\AppFramework\Db\QBMapper;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\IDBConnection;
|
||||
|
|
@ -210,4 +211,23 @@ class UserStatusMapper extends QBMapper {
|
|||
|
||||
$qb->executeStatement();
|
||||
}
|
||||
|
||||
public function getAvailabilityFromPropertiesTable(string $userId): ?string {
|
||||
$propertyPath = 'calendars/' . $userId . '/inbox';
|
||||
$propertyName = '{' . Plugin::NS_CALDAV . '}calendar-availability';
|
||||
|
||||
$query = $this->db->getQueryBuilder();
|
||||
$query->select('propertyvalue')
|
||||
->from('properties')
|
||||
->where($query->expr()->eq('userid', $query->createNamedParameter($userId)))
|
||||
->andWhere($query->expr()->eq('propertypath', $query->createNamedParameter($propertyPath)))
|
||||
->andWhere($query->expr()->eq('propertyname', $query->createNamedParameter($propertyName)))
|
||||
->setMaxResults(1);
|
||||
|
||||
$result = $query->executeQuery();
|
||||
$property = $result->fetchOne();
|
||||
$result->closeCursor();
|
||||
|
||||
return ($property === false ? null : $property);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ class UserLiveStatusListener implements IEventListener {
|
|||
$userStatus->setIsUserDefined(false);
|
||||
}
|
||||
|
||||
// If the status is user-defined and one of the persistent statuses, we
|
||||
// If the status is user-defined and one of the persistent status, we
|
||||
// will not override it.
|
||||
if ($userStatus->getIsUserDefined() &&
|
||||
\in_array($userStatus->getStatus(), StatusService::PERSISTENT_STATUSES, true)) {
|
||||
|
|
|
|||
|
|
@ -202,6 +202,8 @@ class PredefinedStatusService {
|
|||
self::REMOTE_WORK,
|
||||
IUserStatus::MESSAGE_CALL,
|
||||
IUserStatus::MESSAGE_AVAILABILITY,
|
||||
IUserStatus::MESSAGE_CALENDAR_BUSY,
|
||||
IUserStatus::MESSAGE_CALENDAR_BUSY_TENTATIVE,
|
||||
], true);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ declare(strict_types=1);
|
|||
*
|
||||
* @author Georg Ehrke <oc.list@georgehrke.com>
|
||||
* @author Joas Schilling <coding@schilljs.com>
|
||||
* @author Anna Larch <anna.larch@gmx.net>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
|
|
@ -26,6 +27,8 @@ declare(strict_types=1);
|
|||
*/
|
||||
namespace OCA\UserStatus\Service;
|
||||
|
||||
use OCA\DAV\CalDAV\Status\Status as CalendarStatus;
|
||||
use OCA\DAV\CalDAV\Status\StatusService as CalendarStatusService;
|
||||
use OCA\UserStatus\Db\UserStatus;
|
||||
use OCA\UserStatus\Db\UserStatusMapper;
|
||||
use OCA\UserStatus\Exception\InvalidClearAtException;
|
||||
|
|
@ -35,10 +38,13 @@ use OCA\UserStatus\Exception\InvalidStatusTypeException;
|
|||
use OCA\UserStatus\Exception\StatusMessageTooLongException;
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
use OCP\Calendar\ISchedulingInformation;
|
||||
use OCP\DB\Exception;
|
||||
use OCP\IConfig;
|
||||
use OCP\IEmojiHelper;
|
||||
use OCP\IUserManager;
|
||||
use OCP\UserStatus\IUserStatus;
|
||||
use function in_array;
|
||||
|
||||
/**
|
||||
* Class StatusService
|
||||
|
|
@ -46,26 +52,9 @@ use OCP\UserStatus\IUserStatus;
|
|||
* @package OCA\UserStatus\Service
|
||||
*/
|
||||
class StatusService {
|
||||
|
||||
/** @var UserStatusMapper */
|
||||
private $mapper;
|
||||
|
||||
/** @var ITimeFactory */
|
||||
private $timeFactory;
|
||||
|
||||
/** @var PredefinedStatusService */
|
||||
private $predefinedStatusService;
|
||||
|
||||
private IEmojiHelper $emojiHelper;
|
||||
|
||||
/** @var bool */
|
||||
private $shareeEnumeration;
|
||||
|
||||
/** @var bool */
|
||||
private $shareeEnumerationInGroupOnly;
|
||||
|
||||
/** @var bool */
|
||||
private $shareeEnumerationPhone;
|
||||
private bool $shareeEnumeration;
|
||||
private bool $shareeEnumerationInGroupOnly;
|
||||
private bool $shareeEnumerationPhone;
|
||||
|
||||
/**
|
||||
* List of priorities ordered by their priority
|
||||
|
|
@ -74,6 +63,7 @@ class StatusService {
|
|||
IUserStatus::ONLINE,
|
||||
IUserStatus::AWAY,
|
||||
IUserStatus::DND,
|
||||
IUserStatus::BUSY,
|
||||
IUserStatus::INVISIBLE,
|
||||
IUserStatus::OFFLINE,
|
||||
];
|
||||
|
|
@ -84,6 +74,7 @@ class StatusService {
|
|||
*/
|
||||
public const PERSISTENT_STATUSES = [
|
||||
IUserStatus::AWAY,
|
||||
IUserStatus::BUSY,
|
||||
IUserStatus::DND,
|
||||
IUserStatus::INVISIBLE,
|
||||
];
|
||||
|
|
@ -94,18 +85,16 @@ class StatusService {
|
|||
/** @var int */
|
||||
public const MAXIMUM_MESSAGE_LENGTH = 80;
|
||||
|
||||
public function __construct(UserStatusMapper $mapper,
|
||||
ITimeFactory $timeFactory,
|
||||
PredefinedStatusService $defaultStatusService,
|
||||
IEmojiHelper $emojiHelper,
|
||||
IConfig $config) {
|
||||
$this->mapper = $mapper;
|
||||
$this->timeFactory = $timeFactory;
|
||||
$this->predefinedStatusService = $defaultStatusService;
|
||||
$this->emojiHelper = $emojiHelper;
|
||||
$this->shareeEnumeration = $config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes';
|
||||
$this->shareeEnumerationInGroupOnly = $this->shareeEnumeration && $config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes';
|
||||
$this->shareeEnumerationPhone = $this->shareeEnumeration && $config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_phone', 'no') === 'yes';
|
||||
public function __construct(private UserStatusMapper $mapper,
|
||||
private ITimeFactory $timeFactory,
|
||||
private PredefinedStatusService $predefinedStatusService,
|
||||
private IEmojiHelper $emojiHelper,
|
||||
private IConfig $config,
|
||||
private IUserManager $userManager,
|
||||
private CalendarStatusService $calendarStatusService) {
|
||||
$this->shareeEnumeration = $this->config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes';
|
||||
$this->shareeEnumerationInGroupOnly = $this->shareeEnumeration && $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_group', 'no') === 'yes';
|
||||
$this->shareeEnumerationPhone = $this->shareeEnumeration && $this->config->getAppValue('core', 'shareapi_restrict_user_enumeration_to_phone', 'no') === 'yes';
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -149,8 +138,37 @@ class StatusService {
|
|||
* @return UserStatus
|
||||
* @throws DoesNotExistException
|
||||
*/
|
||||
public function findByUserId(string $userId):UserStatus {
|
||||
return $this->processStatus($this->mapper->findByUserId($userId));
|
||||
public function findByUserId(string $userId): UserStatus {
|
||||
$userStatus = $this->mapper->findByUserId($userId);
|
||||
// If the status is user-defined and one of the persistent status, we
|
||||
// will not override it.
|
||||
if ($userStatus->getIsUserDefined() && \in_array($userStatus->getStatus(), StatusService::PERSISTENT_STATUSES, true)) {
|
||||
return $this->processStatus($userStatus);
|
||||
}
|
||||
|
||||
$calendarStatus = $this->getCalendarStatus($userId);
|
||||
// We found no status from the calendar, proceed with the existing status
|
||||
if($calendarStatus === null) {
|
||||
return $this->processStatus($userStatus);
|
||||
}
|
||||
|
||||
// if we have the same status result for the calendar and the current status,
|
||||
// and a custom message to boot, we leave the existing status alone
|
||||
// as to not overwrite a custom message / emoji
|
||||
if($userStatus->getIsUserDefined()
|
||||
&& $calendarStatus->getStatus() === $userStatus->getStatus()
|
||||
&& !empty($userStatus->getCustomMessage())) {
|
||||
return $this->processStatus($userStatus);
|
||||
}
|
||||
|
||||
// If the new status is null, there's already an identical status in place
|
||||
$newUserStatus = $this->setUserStatus($userId,
|
||||
$calendarStatus->getStatus(),
|
||||
$calendarStatus->getMessage() ?? IUserStatus::MESSAGE_AVAILABILITY,
|
||||
true,
|
||||
$calendarStatus->getCustomMessage() ?? '');
|
||||
|
||||
return $newUserStatus === null ? $this->processStatus($userStatus) : $this->processStatus($newUserStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -183,9 +201,12 @@ class StatusService {
|
|||
}
|
||||
|
||||
// Check if status-type is valid
|
||||
if (!\in_array($status, self::PRIORITY_ORDERED_STATUSES, true)) {
|
||||
if (!in_array($status, self::PRIORITY_ORDERED_STATUSES, true)) {
|
||||
throw new InvalidStatusTypeException('Status-type "' . $status . '" is not supported');
|
||||
}
|
||||
|
||||
|
||||
|
||||
if ($statusTimestamp === null) {
|
||||
$statusTimestamp = $this->timeFactory->getTime();
|
||||
}
|
||||
|
|
@ -255,11 +276,12 @@ class StatusService {
|
|||
* @throws InvalidMessageIdException
|
||||
*/
|
||||
public function setUserStatus(string $userId,
|
||||
string $status,
|
||||
string $messageId,
|
||||
bool $createBackup): void {
|
||||
string $status,
|
||||
string $messageId,
|
||||
bool $createBackup,
|
||||
string $customMessage = null): ?UserStatus {
|
||||
// Check if status-type is valid
|
||||
if (!\in_array($status, self::PRIORITY_ORDERED_STATUSES, true)) {
|
||||
if (!in_array($status, self::PRIORITY_ORDERED_STATUSES, true)) {
|
||||
throw new InvalidStatusTypeException('Status-type "' . $status . '" is not supported');
|
||||
}
|
||||
|
||||
|
|
@ -269,7 +291,7 @@ class StatusService {
|
|||
|
||||
if ($createBackup) {
|
||||
if ($this->backupCurrentStatus($userId) === false) {
|
||||
return; // Already a status set automatically => abort.
|
||||
return null; // Already a status set automatically => abort.
|
||||
}
|
||||
|
||||
// If we just created the backup
|
||||
|
|
@ -290,15 +312,14 @@ class StatusService {
|
|||
$userStatus->setIsBackup(false);
|
||||
$userStatus->setMessageId($messageId);
|
||||
$userStatus->setCustomIcon(null);
|
||||
$userStatus->setCustomMessage(null);
|
||||
$userStatus->setCustomMessage($customMessage);
|
||||
$userStatus->setClearAt(null);
|
||||
$userStatus->setStatusMessageTimestamp($this->timeFactory->now()->getTimestamp());
|
||||
|
||||
if ($userStatus->getId() !== null) {
|
||||
$this->mapper->update($userStatus);
|
||||
return;
|
||||
return $this->mapper->update($userStatus);
|
||||
}
|
||||
$this->mapper->insert($userStatus);
|
||||
return $this->mapper->insert($userStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -561,4 +582,35 @@ class StatusService {
|
|||
// For users that matched restore the previous status
|
||||
$this->mapper->restoreBackupStatuses($restoreIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate a users' status according to their availabilit settings and their calendar
|
||||
* events
|
||||
*
|
||||
* There are 4 predefined types of FBTYPE - 'FREE', 'BUSY', 'BUSY-UNAVAILABLE', 'BUSY-TENTATIVE',
|
||||
* but 'X-' properties are possible
|
||||
*
|
||||
* @link https://icalendar.org/iCalendar-RFC-5545/3-2-9-free-busy-time-type.html
|
||||
*
|
||||
* The status will be changed for types
|
||||
* - 'BUSY'
|
||||
* - 'BUSY-UNAVAILABLE' (ex.: when a VAVILABILITY setting is in effect)
|
||||
* - 'BUSY-TENTATIVE' (ex.: an event has been accepted tentatively)
|
||||
* and all FREEBUSY components without a type (implicitly a 'BUSY' status)
|
||||
*
|
||||
* 'X-' properties are not handled for now
|
||||
*
|
||||
* @param string $userId
|
||||
* @return CalendarStatus|null
|
||||
*/
|
||||
public function getCalendarStatus(string $userId): ?CalendarStatus {
|
||||
$user = $this->userManager->get($userId);
|
||||
if ($user === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$availability = $this->mapper->getAvailabilityFromPropertiesTable($userId);
|
||||
|
||||
return $this->calendarStatusService->processCalendarAvailability($user, $availability);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ export default {
|
|||
return this.$t('user_status', 'Online')
|
||||
|
||||
case 'away':
|
||||
case 'busy':
|
||||
return this.$t('user_status', 'Away')
|
||||
|
||||
case 'dnd':
|
||||
|
|
@ -79,6 +80,7 @@ export default {
|
|||
return 'icon-user-status-online'
|
||||
|
||||
case 'away':
|
||||
case 'busy':
|
||||
return 'icon-user-status-away'
|
||||
|
||||
case 'dnd':
|
||||
|
|
|
|||
|
|
@ -28,6 +28,9 @@ namespace OCA\UserStatus\Tests\Service;
|
|||
|
||||
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
|
||||
use OC\DB\Exceptions\DbalException;
|
||||
use OC\User\User;
|
||||
use OCA\DAV\CalDAV\Status\Status;
|
||||
use OCA\DAV\CalDAV\Status\StatusService as CalendarStatusService;
|
||||
use OCA\UserStatus\Db\UserStatus;
|
||||
use OCA\UserStatus\Db\UserStatusMapper;
|
||||
use OCA\UserStatus\Exception\InvalidClearAtException;
|
||||
|
|
@ -40,30 +43,40 @@ use OCA\UserStatus\Service\StatusService;
|
|||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
use OCP\DB\Exception;
|
||||
use OCP\EventDispatcher\IEventDispatcher;
|
||||
use OCP\IConfig;
|
||||
use OCP\IEmojiHelper;
|
||||
use OCP\IUser;
|
||||
use OCP\IUserBackend;
|
||||
use OCP\IUserManager;
|
||||
use OCP\UserStatus\IUserStatus;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Test\TestCase;
|
||||
|
||||
class StatusServiceTest extends TestCase {
|
||||
|
||||
/** @var UserStatusMapper|\PHPUnit\Framework\MockObject\MockObject */
|
||||
/** @var UserStatusMapper|MockObject */
|
||||
private $mapper;
|
||||
|
||||
/** @var ITimeFactory|\PHPUnit\Framework\MockObject\MockObject */
|
||||
/** @var ITimeFactory|MockObject */
|
||||
private $timeFactory;
|
||||
|
||||
/** @var PredefinedStatusService|\PHPUnit\Framework\MockObject\MockObject */
|
||||
/** @var PredefinedStatusService|MockObject */
|
||||
private $predefinedStatusService;
|
||||
|
||||
/** @var IEmojiHelper|\PHPUnit\Framework\MockObject\MockObject */
|
||||
/** @var IEmojiHelper|MockObject */
|
||||
private $emojiHelper;
|
||||
|
||||
/** @var IConfig|\PHPUnit\Framework\MockObject\MockObject */
|
||||
/** @var IConfig|MockObject */
|
||||
private $config;
|
||||
|
||||
/** @var StatusService */
|
||||
private $service;
|
||||
/** @var IUserManager|MockObject */
|
||||
private $userManager;
|
||||
|
||||
/** @var CalendarStatusService|MockObject */
|
||||
private $calendarStatusService;
|
||||
|
||||
private StatusService $service;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
|
@ -72,6 +85,8 @@ class StatusServiceTest extends TestCase {
|
|||
$this->timeFactory = $this->createMock(ITimeFactory::class);
|
||||
$this->predefinedStatusService = $this->createMock(PredefinedStatusService::class);
|
||||
$this->emojiHelper = $this->createMock(IEmojiHelper::class);
|
||||
$this->userManager = $this->createMock(IUserManager::class);
|
||||
$this->calendarStatusService = $this->createMock(CalendarStatusService::class);
|
||||
|
||||
$this->config = $this->createMock(IConfig::class);
|
||||
|
||||
|
|
@ -85,7 +100,10 @@ class StatusServiceTest extends TestCase {
|
|||
$this->timeFactory,
|
||||
$this->predefinedStatusService,
|
||||
$this->emojiHelper,
|
||||
$this->config);
|
||||
$this->config,
|
||||
$this->userManager,
|
||||
$this->calendarStatusService,
|
||||
);
|
||||
}
|
||||
|
||||
public function testFindAll(): void {
|
||||
|
|
@ -139,7 +157,10 @@ class StatusServiceTest extends TestCase {
|
|||
$this->timeFactory,
|
||||
$this->predefinedStatusService,
|
||||
$this->emojiHelper,
|
||||
$this->config);
|
||||
$this->config,
|
||||
$this->userManager,
|
||||
$this->calendarStatusService,
|
||||
);
|
||||
|
||||
$this->assertEquals([], $this->service->findAllRecentStatusChanges(20, 50));
|
||||
|
||||
|
|
@ -156,21 +177,14 @@ class StatusServiceTest extends TestCase {
|
|||
$this->timeFactory,
|
||||
$this->predefinedStatusService,
|
||||
$this->emojiHelper,
|
||||
$this->config);
|
||||
$this->config,
|
||||
$this->userManager,
|
||||
$this->calendarStatusService,
|
||||
);
|
||||
|
||||
$this->assertEquals([], $this->service->findAllRecentStatusChanges(20, 50));
|
||||
}
|
||||
|
||||
public function testFindByUserId(): void {
|
||||
$status = $this->createMock(UserStatus::class);
|
||||
$this->mapper->expects($this->once())
|
||||
->method('findByUserId')
|
||||
->with('john.doe')
|
||||
->willReturn($status);
|
||||
|
||||
$this->assertEquals($status, $this->service->findByUserId('john.doe'));
|
||||
}
|
||||
|
||||
public function testFindByUserIdDoesNotExist(): void {
|
||||
$this->mapper->expects($this->once())
|
||||
->method('findByUserId')
|
||||
|
|
@ -825,4 +839,295 @@ class StatusServiceTest extends TestCase {
|
|||
|
||||
$this->service->revertMultipleUserStatus(['john', 'nobackup', 'backuponly', 'nobackupanddnd'], 'call');
|
||||
}
|
||||
|
||||
public function testCalendarAvailabilityNoUser(): void {
|
||||
$userId = 'admin';
|
||||
|
||||
$this->userManager->expects(self::once())
|
||||
->method('get')
|
||||
->with($userId)
|
||||
->willReturn(null);
|
||||
$this->mapper->expects(self::never())
|
||||
->method('getAvailabilityFromPropertiesTable');
|
||||
$this->calendarStatusService->expects(self::never())
|
||||
->method('processCalendarAvailability');
|
||||
|
||||
$this->service->getCalendarStatus($userId);
|
||||
}
|
||||
|
||||
public function testCalendarAvailabilityNoVavailablility(): void {
|
||||
$user = $this->createConfiguredMock(IUser::class, [
|
||||
'getUID' => 'admin',
|
||||
'getEMailAddress' => 'test@test.com',
|
||||
]);
|
||||
|
||||
$this->userManager->expects(self::once())
|
||||
->method('get')
|
||||
->with($user->getUID())
|
||||
->willReturn($user);
|
||||
$this->mapper->expects(self::once())
|
||||
->method('getAvailabilityFromPropertiesTable')
|
||||
->willReturn('');
|
||||
$this->calendarStatusService->expects(self::once())
|
||||
->method('processCalendarAvailability')
|
||||
->with($user, '')
|
||||
->willReturn(null);
|
||||
|
||||
$this->service->getCalendarStatus($user->getUID());
|
||||
}
|
||||
|
||||
public function testCalendarAvailabilityVavailablilityAvailable(): void {
|
||||
$user = $this->createConfiguredMock(IUser::class, [
|
||||
'getUID' => 'admin',
|
||||
'getEMailAddress' => 'test@test.com',
|
||||
]);
|
||||
|
||||
$vavailability = <<<EOF
|
||||
BEGIN:VCALENDAR
|
||||
PRODID:Nextcloud DAV app
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:Europe/Vienna
|
||||
BEGIN:STANDARD
|
||||
TZNAME:CET
|
||||
TZOFFSETFROM:+0200
|
||||
TZOFFSETTO:+0100
|
||||
DTSTART:19701025T030000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
|
||||
END:STANDARD
|
||||
BEGIN:DAYLIGHT
|
||||
TZNAME:CEST
|
||||
TZOFFSETFROM:+0100
|
||||
TZOFFSETTO:+0200
|
||||
DTSTART:19700329T020000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
|
||||
END:DAYLIGHT
|
||||
END:VTIMEZONE
|
||||
BEGIN:VAVAILABILITY
|
||||
BEGIN:AVAILABLE
|
||||
DTSTART;TZID=Europe/Vienna:20231025T000000
|
||||
DTEND;TZID=Europe/Vienna:20231025T235900
|
||||
UID:d866782e-e003-4906-9ece-303f270a2c6b
|
||||
RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU
|
||||
END:AVAILABLE
|
||||
END:VAVAILABILITY
|
||||
END:VCALENDAR
|
||||
EOF;
|
||||
$status = new Status(IUserStatus::AWAY);
|
||||
$this->userManager->expects(self::once())
|
||||
->method('get')
|
||||
->with($user->getUID())
|
||||
->willReturn($user);
|
||||
$this->mapper->expects(self::once())
|
||||
->method('getAvailabilityFromPropertiesTable')
|
||||
->willReturn($vavailability);
|
||||
$this->calendarStatusService->expects(self::once())
|
||||
->method('processCalendarAvailability')
|
||||
->with($user, $vavailability)
|
||||
->willReturn($status);
|
||||
|
||||
$this->service->getCalendarStatus($user->getUID());
|
||||
}
|
||||
|
||||
public function testCalendarAvailabilityVavailablilityUpdate(): void {
|
||||
$user = $this->createConfiguredMock(IUser::class, [
|
||||
'getUID' => 'admin',
|
||||
'getEMailAddress' => 'test@test.com',
|
||||
]);
|
||||
$calDavStatus = new Status(IUserStatus::BUSY, 'meeting', 'In a meeting');
|
||||
$vavailability = <<<EOF
|
||||
BEGIN:VCALENDAR
|
||||
PRODID:Nextcloud DAV app
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:Europe/Vienna
|
||||
BEGIN:STANDARD
|
||||
TZNAME:CET
|
||||
TZOFFSETFROM:+0200
|
||||
TZOFFSETTO:+0100
|
||||
DTSTART:19701025T030000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU
|
||||
END:STANDARD
|
||||
BEGIN:DAYLIGHT
|
||||
TZNAME:CEST
|
||||
TZOFFSETFROM:+0100
|
||||
TZOFFSETTO:+0200
|
||||
DTSTART:19700329T020000
|
||||
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU
|
||||
END:DAYLIGHT
|
||||
END:VTIMEZONE
|
||||
BEGIN:VAVAILABILITY
|
||||
BEGIN:AVAILABLE
|
||||
DTSTART;TZID=Europe/Vienna:20231025T000000
|
||||
DTEND;TZID=Europe/Vienna:20231025T235900
|
||||
UID:d866782e-e003-4906-9ece-303f270a2c6b
|
||||
RRULE:FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR,SA,SU
|
||||
END:AVAILABLE
|
||||
END:VAVAILABILITY
|
||||
END:VCALENDAR
|
||||
EOF;
|
||||
$this->userManager->expects(self::once())
|
||||
->method('get')
|
||||
->with($user->getUID())
|
||||
->willReturn($user);
|
||||
$this->mapper->expects(self::once())
|
||||
->method('getAvailabilityFromPropertiesTable')
|
||||
->willReturn($vavailability);
|
||||
$this->calendarStatusService->expects(self::once())
|
||||
->method('processCalendarAvailability')
|
||||
->with($user, $vavailability)
|
||||
->willReturn($calDavStatus);
|
||||
|
||||
$this->service->getCalendarStatus($user->getUID());
|
||||
}
|
||||
|
||||
public function testFindByUserIdUserDefinedAndPersistent(): void {
|
||||
$status = new UserStatus();
|
||||
$status->setIsUserDefined(true);
|
||||
$status->setStatus(IUserStatus::DND);
|
||||
|
||||
$this->mapper->expects($this->once())
|
||||
->method('findByUserId')
|
||||
->with('admin')
|
||||
->willReturn($status);
|
||||
$this->mapper->expects(self::never())
|
||||
->method('getAvailabilityFromPropertiesTable');
|
||||
$this->calendarStatusService->expects(self::never())
|
||||
->method('processCalendarAvailability');
|
||||
|
||||
$this->assertEquals($status, $this->service->findByUserId('admin'));
|
||||
}
|
||||
|
||||
public function testFindByUserIdUserDefinedNoCalStatus(): void {
|
||||
$user = $this->createConfiguredMock(IUser::class, [
|
||||
'getUID' => 'admin',
|
||||
'getEMailAddress' => 'test@test.com',
|
||||
]);
|
||||
$status = new UserStatus();
|
||||
$status->setIsUserDefined(true);
|
||||
$status->setStatus(IUserStatus::ONLINE);
|
||||
|
||||
$this->mapper->expects($this->once())
|
||||
->method('findByUserId')
|
||||
->with($user->getUID())
|
||||
->willReturn($status);
|
||||
$this->userManager->expects(self::once())
|
||||
->method('get')
|
||||
->willReturn($user);
|
||||
$this->mapper->expects(self::once())
|
||||
->method('getAvailabilityFromPropertiesTable')
|
||||
->willReturn('');
|
||||
$this->calendarStatusService->expects(self::once())
|
||||
->method('processCalendarAvailability')
|
||||
->with($user, '')
|
||||
->willReturn(null);
|
||||
|
||||
$this->assertEquals($status, $this->service->findByUserId('admin'));
|
||||
}
|
||||
|
||||
public function testFindByUserIdUserDefinedCalStatusIdentical(): void {
|
||||
$user = $this->createConfiguredMock(IUser::class, [
|
||||
'getUID' => 'admin',
|
||||
'getEMailAddress' => 'test@test.com',
|
||||
]);
|
||||
$calDavStatus = new Status(IUserStatus::ONLINE);
|
||||
$userStatus = new UserStatus();
|
||||
$userStatus->setStatus(IUserStatus::ONLINE);
|
||||
$userStatus->setIsUserDefined(true);
|
||||
$userStatus->setCustomMessage('Test');
|
||||
|
||||
$this->mapper->expects(self::once())
|
||||
->method('findByUserId')
|
||||
->with($user->getUID())
|
||||
->willReturn($userStatus);
|
||||
$this->userManager->expects(self::once())
|
||||
->method('get')
|
||||
->willReturn($user);
|
||||
$this->mapper->expects(self::once())
|
||||
->method('getAvailabilityFromPropertiesTable')
|
||||
->willReturn('');
|
||||
$this->calendarStatusService->expects(self::once())
|
||||
->method('processCalendarAvailability')
|
||||
->with($user, '')
|
||||
->willReturn($calDavStatus);
|
||||
|
||||
$this->assertEquals($userStatus, $this->service->findByUserId('admin'));
|
||||
}
|
||||
|
||||
public function testFindByUserIdUserDefinedCalStatusUpdate(): void {
|
||||
$user = $this->createConfiguredMock(IUser::class, [
|
||||
'getUID' => 'admin',
|
||||
'getEMailAddress' => 'test@test.com',
|
||||
]);
|
||||
$calDavStatus = new Status(IUserStatus::BUSY, 'meeting', 'In a meeting');
|
||||
|
||||
$oldStatus = new UserStatus();
|
||||
$oldStatus->setId(42);
|
||||
$oldStatus->setUserId($user->getUID());
|
||||
$oldStatus->setStatus(IUserStatus::ONLINE);
|
||||
$oldStatus->setStatusTimestamp(0);
|
||||
$oldStatus->setIsUserDefined(true);
|
||||
|
||||
$expected = new UserStatus();
|
||||
$expected->setUserId($user->getUID());
|
||||
$expected->setStatus(IUserStatus::BUSY);
|
||||
$expected->setStatusTimestamp(0);
|
||||
$expected->setIsUserDefined(true);
|
||||
$expected->setIsBackup(false);
|
||||
|
||||
$this->mapper->expects(self::once())
|
||||
->method('findByUserId')
|
||||
->with($user->getUID())
|
||||
->willReturn($oldStatus);
|
||||
$this->userManager->expects(self::once())
|
||||
->method('get')
|
||||
->willReturn($user);
|
||||
$this->mapper->expects(self::once())
|
||||
->method('getAvailabilityFromPropertiesTable')
|
||||
->willReturn('');
|
||||
$this->mapper->expects(self::once())
|
||||
->method('createBackupStatus')
|
||||
->with($user->getUID())
|
||||
->willReturn(true);
|
||||
$this->calendarStatusService->expects(self::once())
|
||||
->method('processCalendarAvailability')
|
||||
->with($user, '')
|
||||
->willReturn($calDavStatus);
|
||||
$this->predefinedStatusService->expects(self::once())
|
||||
->method('isValidId')
|
||||
->with($calDavStatus->getMessage())
|
||||
->willReturn(true);
|
||||
$this->mapper->expects(self::once())
|
||||
->method('insert')
|
||||
->willReturn($expected);
|
||||
|
||||
$actual = $this->service->findByUserId('admin');
|
||||
$this->assertEquals($expected->getStatus(), $actual->getStatus());
|
||||
$this->assertEquals($expected->getCustomMessage(), $actual->getCustomMessage());
|
||||
}
|
||||
|
||||
public function testFindByUserIdSystemDefined(): void {
|
||||
$user = $this->createConfiguredMock(IUser::class, [
|
||||
'getUID' => 'admin',
|
||||
'getEMailAddress' => 'test@test.com',
|
||||
]);
|
||||
$status = new UserStatus();
|
||||
$status->setIsUserDefined(false);
|
||||
$status->setStatus(IUserStatus::ONLINE);
|
||||
|
||||
$this->mapper->expects($this->once())
|
||||
->method('findByUserId')
|
||||
->with($user->getUID())
|
||||
->willReturn($status);
|
||||
$this->userManager->expects(self::once())
|
||||
->method('get')
|
||||
->willReturn($user);
|
||||
$this->mapper->expects(self::once())
|
||||
->method('getAvailabilityFromPropertiesTable')
|
||||
->willReturn('');
|
||||
$this->calendarStatusService->expects(self::once())
|
||||
->method('processCalendarAvailability')
|
||||
->with($user, '')
|
||||
->willReturn(null);
|
||||
|
||||
$this->assertEquals($status, $this->service->findByUserId('admin'));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
4
dist/user_status-menu.js
vendored
4
dist/user_status-menu.js
vendored
File diff suppressed because one or more lines are too long
2
dist/user_status-menu.js.map
vendored
2
dist/user_status-menu.js.map
vendored
File diff suppressed because one or more lines are too long
|
|
@ -51,6 +51,12 @@ interface IUserStatus {
|
|||
*/
|
||||
public const DND = 'dnd';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
* @since 28.0.0
|
||||
*/
|
||||
public const BUSY = 'busy';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
* @since 20.0.0
|
||||
|
|
@ -75,6 +81,18 @@ interface IUserStatus {
|
|||
*/
|
||||
public const MESSAGE_AVAILABILITY = 'availability';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
* @since 28.0.0
|
||||
*/
|
||||
public const MESSAGE_CALENDAR_BUSY = 'meeting';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
* @since 28.0.0
|
||||
*/
|
||||
public const MESSAGE_CALENDAR_BUSY_TENTATIVE = 'busy-tentative';
|
||||
|
||||
/**
|
||||
* Get the user this status is connected to
|
||||
*
|
||||
|
|
|
|||
Loading…
Reference in a new issue