mirror of
https://github.com/nextcloud/server.git
synced 2026-06-08 00:02:54 -04:00
Merge pull request #41798 from nextcloud/enh/stable28/add-ooo-and-remove-user-status
[stable28] enh(userstatus): add OOO automation and remove calendar automation
This commit is contained in:
commit
699ee7a1d8
26 changed files with 643 additions and 1522 deletions
|
|
@ -30,6 +30,10 @@ use OCP\BackgroundJob\TimedJob;
|
|||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\IConfig;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\IUser;
|
||||
use OCP\IUserManager;
|
||||
use OCP\User\IAvailabilityCoordinator;
|
||||
use OCP\User\IOutOfOfficeData;
|
||||
use OCP\UserStatus\IManager;
|
||||
use OCP\UserStatus\IUserStatus;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
|
@ -39,24 +43,15 @@ use Sabre\VObject\Reader;
|
|||
use Sabre\VObject\Recur\RRuleIterator;
|
||||
|
||||
class UserStatusAutomation extends TimedJob {
|
||||
protected IDBConnection $connection;
|
||||
protected IJobList $jobList;
|
||||
protected LoggerInterface $logger;
|
||||
protected IManager $manager;
|
||||
protected IConfig $config;
|
||||
|
||||
public function __construct(ITimeFactory $timeFactory,
|
||||
IDBConnection $connection,
|
||||
IJobList $jobList,
|
||||
LoggerInterface $logger,
|
||||
IManager $manager,
|
||||
IConfig $config) {
|
||||
public function __construct(private ITimeFactory $timeFactory,
|
||||
private IDBConnection $connection,
|
||||
private IJobList $jobList,
|
||||
private LoggerInterface $logger,
|
||||
private IManager $manager,
|
||||
private IConfig $config,
|
||||
private IAvailabilityCoordinator $coordinator,
|
||||
private IUserManager $userManager) {
|
||||
parent::__construct($timeFactory);
|
||||
$this->connection = $connection;
|
||||
$this->jobList = $jobList;
|
||||
$this->logger = $logger;
|
||||
$this->manager = $manager;
|
||||
$this->config = $config;
|
||||
|
||||
// Interval 0 might look weird, but the last_checked is always moved
|
||||
// to the next time we need this and then it's 0 seconds ago.
|
||||
|
|
@ -74,98 +69,31 @@ class UserStatusAutomation extends TimedJob {
|
|||
}
|
||||
|
||||
$userId = $argument['userId'];
|
||||
$automationEnabled = $this->config->getUserValue($userId, 'dav', 'user_status_automation', 'no') === 'yes';
|
||||
if (!$automationEnabled) {
|
||||
$this->logger->info('Removing ' . self::class . ' background job for user "' . $userId . '" because the setting is disabled');
|
||||
$this->jobList->remove(self::class, $argument);
|
||||
$user = $this->userManager->get($userId);
|
||||
if($user === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$ooo = $this->coordinator->getCurrentOutOfOfficeData($user);
|
||||
|
||||
$continue = $this->processOutOfOfficeData($user, $ooo);
|
||||
if($continue === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
$property = $this->getAvailabilityFromPropertiesTable($userId);
|
||||
$hasDndForOfficeHours = $this->config->getUserValue($userId, 'dav', 'user_status_automation', 'no') === 'yes';
|
||||
|
||||
if (!$property) {
|
||||
$this->logger->info('Removing ' . self::class . ' background job for user "' . $userId . '" because the user has no availability settings');
|
||||
// We found no ooo data and no availability settings, so we need to delete the job because there is no next runtime
|
||||
$this->logger->info('Removing ' . self::class . ' background job for user "' . $userId . '" because the user has no valid availability rules and no OOO data set');
|
||||
$this->jobList->remove(self::class, $argument);
|
||||
$this->manager->revertUserStatus($user->getUID(), IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND);
|
||||
$this->manager->revertUserStatus($user->getUID(), IUserStatus::MESSAGE_VACATION, IUserStatus::DND);
|
||||
return;
|
||||
}
|
||||
|
||||
$isCurrentlyAvailable = false;
|
||||
$nextPotentialToggles = [];
|
||||
|
||||
$now = $this->time->getDateTime();
|
||||
$lastMidnight = (clone $now)->setTime(0, 0);
|
||||
|
||||
$vObject = Reader::read($property);
|
||||
foreach ($vObject->getComponents() as $component) {
|
||||
if ($component->name !== 'VAVAILABILITY') {
|
||||
continue;
|
||||
}
|
||||
/** @var VAvailability $component */
|
||||
$availables = $component->getComponents();
|
||||
foreach ($availables as $available) {
|
||||
/** @var Available $available */
|
||||
if ($available->name === 'AVAILABLE') {
|
||||
/** @var \DateTimeImmutable $originalStart */
|
||||
/** @var \DateTimeImmutable $originalEnd */
|
||||
[$originalStart, $originalEnd] = $available->getEffectiveStartEnd();
|
||||
|
||||
// Little shenanigans to fix the automation on the day the rules were adjusted
|
||||
// Otherwise the $originalStart would match rules for Thursdays on a Friday, etc.
|
||||
// So we simply wind back a week and then fastForward to the next occurrence
|
||||
// since today's midnight, which then also accounts for the week days.
|
||||
$effectiveStart = \DateTime::createFromImmutable($originalStart)->sub(new \DateInterval('P7D'));
|
||||
$effectiveEnd = \DateTime::createFromImmutable($originalEnd)->sub(new \DateInterval('P7D'));
|
||||
|
||||
try {
|
||||
$it = new RRuleIterator((string) $available->RRULE, $effectiveStart);
|
||||
$it->fastForward($lastMidnight);
|
||||
|
||||
$startToday = $it->current();
|
||||
if ($startToday && $startToday <= $now) {
|
||||
$duration = $effectiveStart->diff($effectiveEnd);
|
||||
$endToday = $startToday->add($duration);
|
||||
if ($endToday > $now) {
|
||||
// User is currently available
|
||||
// Also queuing the end time as next status toggle
|
||||
$isCurrentlyAvailable = true;
|
||||
$nextPotentialToggles[] = $endToday->getTimestamp();
|
||||
}
|
||||
|
||||
// Availability enabling already done for today,
|
||||
// so jump to the next recurrence to find the next status toggle
|
||||
$it->next();
|
||||
}
|
||||
|
||||
if ($it->current()) {
|
||||
$nextPotentialToggles[] = $it->current()->getTimestamp();
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($nextPotentialToggles)) {
|
||||
$this->logger->info('Removing ' . self::class . ' background job for user "' . $userId . '" because the user has no valid availability rules set');
|
||||
$this->jobList->remove(self::class, $argument);
|
||||
$this->manager->revertUserStatus($userId, IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND);
|
||||
return;
|
||||
}
|
||||
|
||||
$nextAutomaticToggle = min($nextPotentialToggles);
|
||||
$this->setLastRunToNextToggleTime($userId, $nextAutomaticToggle - 1);
|
||||
|
||||
if ($isCurrentlyAvailable) {
|
||||
$this->logger->debug('User is currently available, reverting DND status if applicable');
|
||||
$this->manager->revertUserStatus($userId, IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND);
|
||||
} else {
|
||||
$this->logger->debug('User is currently NOT available, reverting call status if applicable and then setting DND');
|
||||
// The DND status automation is more important than the "Away - In call" so we also restore that one if it exists.
|
||||
$this->manager->revertUserStatus($userId, IUserStatus::MESSAGE_CALL, IUserStatus::AWAY);
|
||||
$this->manager->setUserStatus($userId, IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND, true);
|
||||
}
|
||||
$this->logger->debug('User status automation ran');
|
||||
$this->processAvailability($property, $user->getUID(), $hasDndForOfficeHours);
|
||||
}
|
||||
|
||||
protected function setLastRunToNextToggleTime(string $userId, int $timestamp): void {
|
||||
|
|
@ -201,4 +129,131 @@ class UserStatusAutomation extends TimedJob {
|
|||
|
||||
return $property;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $property
|
||||
* @param $userId
|
||||
* @param $argument
|
||||
* @return void
|
||||
*/
|
||||
private function processAvailability(string $property, string $userId, bool $hasDndForOfficeHours): void {
|
||||
$isCurrentlyAvailable = false;
|
||||
$nextPotentialToggles = [];
|
||||
|
||||
$now = $this->time->getDateTime();
|
||||
$lastMidnight = (clone $now)->setTime(0, 0);
|
||||
|
||||
$vObject = Reader::read($property);
|
||||
foreach ($vObject->getComponents() as $component) {
|
||||
if ($component->name !== 'VAVAILABILITY') {
|
||||
continue;
|
||||
}
|
||||
/** @var VAvailability $component */
|
||||
$availables = $component->getComponents();
|
||||
foreach ($availables as $available) {
|
||||
/** @var Available $available */
|
||||
if ($available->name === 'AVAILABLE') {
|
||||
/** @var \DateTimeImmutable $originalStart */
|
||||
/** @var \DateTimeImmutable $originalEnd */
|
||||
[$originalStart, $originalEnd] = $available->getEffectiveStartEnd();
|
||||
|
||||
// Little shenanigans to fix the automation on the day the rules were adjusted
|
||||
// Otherwise the $originalStart would match rules for Thursdays on a Friday, etc.
|
||||
// So we simply wind back a week and then fastForward to the next occurrence
|
||||
// since today's midnight, which then also accounts for the week days.
|
||||
$effectiveStart = \DateTime::createFromImmutable($originalStart)->sub(new \DateInterval('P7D'));
|
||||
$effectiveEnd = \DateTime::createFromImmutable($originalEnd)->sub(new \DateInterval('P7D'));
|
||||
|
||||
try {
|
||||
$it = new RRuleIterator((string)$available->RRULE, $effectiveStart);
|
||||
$it->fastForward($lastMidnight);
|
||||
|
||||
$startToday = $it->current();
|
||||
if ($startToday && $startToday <= $now) {
|
||||
$duration = $effectiveStart->diff($effectiveEnd);
|
||||
$endToday = $startToday->add($duration);
|
||||
if ($endToday > $now) {
|
||||
// User is currently available
|
||||
// Also queuing the end time as next status toggle
|
||||
$isCurrentlyAvailable = true;
|
||||
$nextPotentialToggles[] = $endToday->getTimestamp();
|
||||
}
|
||||
|
||||
// Availability enabling already done for today,
|
||||
// so jump to the next recurrence to find the next status toggle
|
||||
$it->next();
|
||||
}
|
||||
|
||||
if ($it->current()) {
|
||||
$nextPotentialToggles[] = $it->current()->getTimestamp();
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$this->logger->error($e->getMessage(), ['exception' => $e]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($nextPotentialToggles)) {
|
||||
$this->logger->info('Removing ' . self::class . ' background job for user "' . $userId . '" because the user has no valid availability rules set');
|
||||
$this->jobList->remove(self::class, ['userId' => $userId]);
|
||||
$this->manager->revertUserStatus($userId, IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND);
|
||||
return;
|
||||
}
|
||||
|
||||
$nextAutomaticToggle = min($nextPotentialToggles);
|
||||
$this->setLastRunToNextToggleTime($userId, $nextAutomaticToggle - 1);
|
||||
|
||||
if ($isCurrentlyAvailable) {
|
||||
$this->logger->debug('User is currently available, reverting DND status if applicable');
|
||||
$this->manager->revertUserStatus($userId, IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND);
|
||||
$this->logger->debug('User status automation ran');
|
||||
return;
|
||||
}
|
||||
|
||||
if(!$hasDndForOfficeHours) {
|
||||
// Office hours are not set to DND, so there is nothing to do.
|
||||
return;
|
||||
}
|
||||
|
||||
$this->logger->debug('User is currently NOT available, reverting call status if applicable and then setting DND');
|
||||
// The DND status automation is more important than the "Away - In call" so we also restore that one if it exists.
|
||||
$this->manager->revertUserStatus($userId, IUserStatus::MESSAGE_CALL, IUserStatus::AWAY);
|
||||
$this->manager->setUserStatus($userId, IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND, true);
|
||||
$this->logger->debug('User status automation ran');
|
||||
}
|
||||
|
||||
private function processOutOfOfficeData(IUser $user, ?IOutOfOfficeData $ooo): bool {
|
||||
if(empty($ooo)) {
|
||||
// Reset the user status if the absence doesn't exist
|
||||
$this->logger->debug('User has no OOO period in effect, reverting DND status if applicable');
|
||||
$this->manager->revertUserStatus($user->getUID(), IUserStatus::MESSAGE_VACATION, IUserStatus::DND);
|
||||
// We need to also run the availability automation
|
||||
return true;
|
||||
}
|
||||
|
||||
if(!$this->coordinator->isInEffect($ooo)) {
|
||||
// Reset the user status if the absence is (no longer) in effect
|
||||
$this->logger->debug('User has no OOO period in effect, reverting DND status if applicable');
|
||||
$this->manager->revertUserStatus($user->getUID(), IUserStatus::MESSAGE_VACATION, IUserStatus::DND);
|
||||
|
||||
if($ooo->getStartDate() > $this->time->getTime()) {
|
||||
// Set the next run to take place at the start of the ooo period if it is in the future
|
||||
// This might be overwritten if there is an availability setting, but we can't determine
|
||||
// if this is the case here
|
||||
$this->setLastRunToNextToggleTime($user->getUID(), $ooo->getStartDate());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->logger->debug('User is currently in an OOO period, reverting other automated status and setting OOO DND status');
|
||||
// Revert both a possible 'CALL - away' and 'office hours - DND' status
|
||||
$this->manager->revertUserStatus($user->getUID(), IUserStatus::MESSAGE_CALL, IUserStatus::DND);
|
||||
$this->manager->revertUserStatus($user->getUID(), IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND);
|
||||
$this->manager->setUserStatus($user->getUID(), IUserStatus::MESSAGE_VACATION, IUserStatus::DND, true, $ooo->getShortMessage());
|
||||
// Run at the end of an ooo period to return to availability / regular user status
|
||||
// If it's overwritten by a custom status in the meantime, there's nothing we can do about it
|
||||
$this->setLastRunToNextToggleTime($user->getUID(), $ooo->getEndDate());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,8 +26,7 @@
|
|||
namespace OCA\DAV\CalDAV\Status;
|
||||
|
||||
class Status {
|
||||
|
||||
public function __construct(private string $status = '', private ?string $message = null, private ?string $customMessage = null) {
|
||||
public function __construct(private string $status = '', private ?string $message = null, private ?string $customMessage = null, private ?int $timestamp = null, private ?string $customEmoji = null) {
|
||||
}
|
||||
|
||||
public function getStatus(): string {
|
||||
|
|
@ -54,5 +53,19 @@ class Status {
|
|||
$this->customMessage = $customMessage;
|
||||
}
|
||||
|
||||
public function setEndTime(?int $timestamp): void {
|
||||
$this->timestamp = $timestamp;
|
||||
}
|
||||
|
||||
public function getEndTime(): ?int {
|
||||
return $this->timestamp;
|
||||
}
|
||||
|
||||
public function getCustomEmoji(): ?string {
|
||||
return $this->customEmoji;
|
||||
}
|
||||
|
||||
public function setCustomEmoji(?string $emoji): void {
|
||||
$this->customEmoji = $emoji;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,7 +66,6 @@ use Sabre\VObject\Component;
|
|||
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,
|
||||
|
|
@ -76,7 +75,7 @@ class StatusService {
|
|||
private FreeBusyGenerator $generator) {
|
||||
}
|
||||
|
||||
public function processCalendarAvailability(User $user, ?string $availability): ?Status {
|
||||
public function processCalendarAvailability(User $user): ?Status {
|
||||
$userId = $user->getUID();
|
||||
$email = $user->getEMailAddress();
|
||||
if($email === null) {
|
||||
|
|
@ -160,8 +159,7 @@ class StatusService {
|
|||
}
|
||||
|
||||
// @todo we can cache that
|
||||
if(empty($availability) && empty($calendarEvents)) {
|
||||
// No availability settings and no calendar events, we can stop here
|
||||
if(empty($calendarEvents)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -181,15 +179,6 @@ class StatusService {
|
|||
$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)) {
|
||||
|
|
@ -200,9 +189,8 @@ class StatusService {
|
|||
$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);
|
||||
return null;
|
||||
}
|
||||
|
||||
/** @var Property $freeBusyProperty */
|
||||
|
|
@ -220,12 +208,10 @@ class StatusService {
|
|||
}
|
||||
$fbType = $fbTypeParameter->getValue();
|
||||
switch ($fbType) {
|
||||
// Ignore BUSY-UNAVAILABLE, that's for the automation
|
||||
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);
|
||||
return new Status(IUserStatus::BUSY, IUserStatus::MESSAGE_CALENDAR_BUSY, $this->l10n->t('In a meeting'));
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,12 +36,14 @@ use OCP\AppFramework\Http\JSONResponse;
|
|||
use OCP\AppFramework\Http\Response;
|
||||
use OCP\IRequest;
|
||||
use OCP\IUserSession;
|
||||
use OCP\User\IAvailabilityCoordinator;
|
||||
|
||||
class AvailabilitySettingsController extends Controller {
|
||||
public function __construct(
|
||||
IRequest $request,
|
||||
private ?IUserSession $userSession,
|
||||
private AbsenceService $absenceService,
|
||||
private IAvailabilityCoordinator $coordinator,
|
||||
) {
|
||||
parent::__construct(Application::APP_ID, $request);
|
||||
}
|
||||
|
|
@ -75,6 +77,7 @@ class AvailabilitySettingsController extends Controller {
|
|||
$status,
|
||||
$message,
|
||||
);
|
||||
$this->coordinator->clearCache($user->getUID());
|
||||
return new JSONResponse($absence);
|
||||
}
|
||||
|
||||
|
|
@ -89,6 +92,7 @@ class AvailabilitySettingsController extends Controller {
|
|||
}
|
||||
|
||||
$this->absenceService->clearAbsence($user);
|
||||
$this->coordinator->clearCache($user->getUID());
|
||||
return new JSONResponse([]);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ declare(strict_types=1);
|
|||
namespace OCA\DAV\Db;
|
||||
|
||||
use DateTime;
|
||||
use DateTimeZone;
|
||||
use Exception;
|
||||
use InvalidArgumentException;
|
||||
use JsonSerializable;
|
||||
|
|
@ -58,6 +57,7 @@ class Absence extends Entity implements JsonSerializable {
|
|||
protected string $lastDay = '';
|
||||
|
||||
protected string $status = '';
|
||||
|
||||
protected string $message = '';
|
||||
|
||||
public function __construct() {
|
||||
|
|
@ -76,7 +76,7 @@ class Absence extends Entity implements JsonSerializable {
|
|||
throw new Exception('Creating out-of-office data without ID');
|
||||
}
|
||||
|
||||
$tz = new DateTimeZone($timezone);
|
||||
$tz = new \DateTimeZone($timezone);
|
||||
$startDate = new DateTime($this->getFirstDay(), $tz);
|
||||
$endDate = new DateTime($this->getLastDay(), $tz);
|
||||
$endDate->setTime(23, 59);
|
||||
|
|
|
|||
|
|
@ -52,21 +52,21 @@ use function rewind;
|
|||
* @template-implements IEventListener<OutOfOfficeScheduledEvent|OutOfOfficeChangedEvent|OutOfOfficeClearedEvent>
|
||||
*/
|
||||
class OutOfOfficeListener implements IEventListener {
|
||||
public function __construct(private ServerFactory $serverFactory,
|
||||
public function __construct(
|
||||
private ServerFactory $serverFactory,
|
||||
private IConfig $appConfig,
|
||||
private LoggerInterface $logger) {
|
||||
private LoggerInterface $logger
|
||||
) {
|
||||
}
|
||||
|
||||
public function handle(Event $event): void {
|
||||
if ($event instanceof OutOfOfficeScheduledEvent) {
|
||||
$userId = $event->getData()->getUser()->getUID();
|
||||
$principal = "principals/users/$userId";
|
||||
|
||||
$calendarNode = $this->getCalendarNode($principal, $userId);
|
||||
if ($calendarNode === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$tz = $calendarNode->getProperties([])['{urn:ietf:params:xml:ns:caldav}calendar-timezone'] ?? null;
|
||||
$vCalendarEvent = $this->createVCalendarEvent($event->getData(), $tz);
|
||||
$stream = fopen('php://memory', 'rb+');
|
||||
|
|
@ -83,7 +83,6 @@ class OutOfOfficeListener implements IEventListener {
|
|||
} elseif ($event instanceof OutOfOfficeChangedEvent) {
|
||||
$userId = $event->getData()->getUser()->getUID();
|
||||
$principal = "principals/users/$userId";
|
||||
|
||||
$calendarNode = $this->getCalendarNode($principal, $userId);
|
||||
if ($calendarNode === null) {
|
||||
return;
|
||||
|
|
@ -110,17 +109,16 @@ class OutOfOfficeListener implements IEventListener {
|
|||
} elseif ($event instanceof OutOfOfficeClearedEvent) {
|
||||
$userId = $event->getData()->getUser()->getUID();
|
||||
$principal = "principals/users/$userId";
|
||||
|
||||
$calendarNode = $this->getCalendarNode($principal, $userId);
|
||||
if ($calendarNode === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$oldEvent = $calendarNode->getChild($this->getEventFileName($event->getData()->getId()));
|
||||
$oldEvent->delete();
|
||||
} catch (NotFound) {
|
||||
// The user must have deleted it or the default calendar changed -> ignore
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,12 +32,16 @@ use OCA\DAV\CalDAV\TimezoneService;
|
|||
use OCA\DAV\Db\Absence;
|
||||
use OCA\DAV\Db\AbsenceMapper;
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
use OCP\BackgroundJob\IJobList;
|
||||
use OCP\Calendar\IManager;
|
||||
use OCP\EventDispatcher\IEventDispatcher;
|
||||
use OCP\IConfig;
|
||||
use OCP\IUser;
|
||||
use OCP\User\Events\OutOfOfficeChangedEvent;
|
||||
use OCP\User\Events\OutOfOfficeClearedEvent;
|
||||
use OCP\User\Events\OutOfOfficeScheduledEvent;
|
||||
use OCP\User\IOutOfOfficeData;
|
||||
|
||||
class AbsenceService {
|
||||
public function __construct(
|
||||
|
|
@ -45,6 +49,9 @@ class AbsenceService {
|
|||
private IEventDispatcher $eventDispatcher,
|
||||
private IJobList $jobList,
|
||||
private TimezoneService $timezoneService,
|
||||
private ITimeFactory $timeFactory,
|
||||
private IConfig $appConfig,
|
||||
private IManager $calendarManager,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -128,4 +135,17 @@ class AbsenceService {
|
|||
);
|
||||
$this->eventDispatcher->dispatchTyped(new OutOfOfficeClearedEvent($eventData));
|
||||
}
|
||||
|
||||
public function getAbsence(string $userId): ?Absence {
|
||||
try {
|
||||
return $this->absenceMapper->findByUserId($userId);
|
||||
} catch (DoesNotExistException $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public function isInEffect(IOutOfOfficeData $absence): bool {
|
||||
$now = $this->timeFactory->getTime();
|
||||
return $absence->getStartDate() <= $now && $absence->getEndDate() >= $now;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,10 +26,14 @@ declare(strict_types=1);
|
|||
|
||||
namespace OCA\DAV\Tests\unit\BackgroundJob;
|
||||
|
||||
use OC\User\OutOfOfficeData;
|
||||
use OCA\DAV\BackgroundJob\UserStatusAutomation;
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
use OCP\BackgroundJob\IJobList;
|
||||
use OCP\IConfig;
|
||||
use OCP\IUser;
|
||||
use OCP\IUserManager;
|
||||
use OCP\User\IAvailabilityCoordinator;
|
||||
use OCP\UserStatus\IManager;
|
||||
use OCP\UserStatus\IUserStatus;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
|
|
@ -46,6 +50,8 @@ class UserStatusAutomationTest extends TestCase {
|
|||
protected MockObject|LoggerInterface $logger;
|
||||
protected MockObject|IManager $statusManager;
|
||||
protected MockObject|IConfig $config;
|
||||
private IAvailabilityCoordinator|MockObject $coordinator;
|
||||
private IUserManager|MockObject $userManager;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
|
@ -55,6 +61,8 @@ class UserStatusAutomationTest extends TestCase {
|
|||
$this->logger = $this->createMock(LoggerInterface::class);
|
||||
$this->statusManager = $this->createMock(IManager::class);
|
||||
$this->config = $this->createMock(IConfig::class);
|
||||
$this->coordinator = $this->createMock(IAvailabilityCoordinator::class);
|
||||
$this->userManager = $this->createMock(IUserManager::class);
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -67,6 +75,8 @@ class UserStatusAutomationTest extends TestCase {
|
|||
$this->logger,
|
||||
$this->statusManager,
|
||||
$this->config,
|
||||
$this->coordinator,
|
||||
$this->userManager,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -78,6 +88,8 @@ class UserStatusAutomationTest extends TestCase {
|
|||
$this->logger,
|
||||
$this->statusManager,
|
||||
$this->config,
|
||||
$this->coordinator,
|
||||
$this->userManager,
|
||||
])
|
||||
->setMethods($methods)
|
||||
->getMock();
|
||||
|
|
@ -95,14 +107,31 @@ class UserStatusAutomationTest extends TestCase {
|
|||
/**
|
||||
* @dataProvider dataRun
|
||||
*/
|
||||
public function testRun(string $ruleDay, string $currentTime, bool $isAvailable): void {
|
||||
public function testRunNoOOO(string $ruleDay, string $currentTime, bool $isAvailable): void {
|
||||
$user = $this->createConfiguredMock(IUser::class, [
|
||||
'getUID' => 'user'
|
||||
]);
|
||||
|
||||
$this->userManager->expects(self::once())
|
||||
->method('get')
|
||||
->willReturn($user);
|
||||
$this->coordinator->expects(self::once())
|
||||
->method('getCurrentOutOfOfficeData')
|
||||
->willReturn(null);
|
||||
$this->config->method('getUserValue')
|
||||
->with('user', 'dav', 'user_status_automation', 'no')
|
||||
->willReturn('yes');
|
||||
|
||||
$this->time->method('getDateTime')
|
||||
->willReturn(new \DateTime($currentTime, new \DateTimeZone('UTC')));
|
||||
|
||||
$this->logger->expects(self::exactly(4))
|
||||
->method('debug');
|
||||
$this->statusManager->expects(self::exactly(2))
|
||||
->method('revertUserStatus');
|
||||
if (!$isAvailable) {
|
||||
$this->statusManager->expects(self::once())
|
||||
->method('setUserStatus')
|
||||
->with('user', IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND, true);
|
||||
}
|
||||
$automation = $this->getAutomationMock(['getAvailabilityFromPropertiesTable']);
|
||||
$automation->method('getAvailabilityFromPropertiesTable')
|
||||
->with('user')
|
||||
|
|
@ -141,63 +170,74 @@ END:AVAILABLE
|
|||
END:VAVAILABILITY
|
||||
END:VCALENDAR');
|
||||
|
||||
if ($isAvailable) {
|
||||
$this->statusManager->expects($this->once())
|
||||
->method('revertUserStatus')
|
||||
->with('user', IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND);
|
||||
} else {
|
||||
$this->statusManager->expects($this->once())
|
||||
->method('revertUserStatus')
|
||||
->with('user', IUserStatus::MESSAGE_CALL, IUserStatus::AWAY);
|
||||
$this->statusManager->expects($this->once())
|
||||
->method('setUserStatus')
|
||||
->with('user', IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND, true);
|
||||
}
|
||||
self::invokePrivate($automation, 'run', [['userId' => 'user']]);
|
||||
}
|
||||
|
||||
public function testRunNoAvailabilityNoOOO(): void {
|
||||
$user = $this->createConfiguredMock(IUser::class, [
|
||||
'getUID' => 'user'
|
||||
]);
|
||||
|
||||
$this->userManager->expects(self::once())
|
||||
->method('get')
|
||||
->willReturn($user);
|
||||
$this->coordinator->expects(self::once())
|
||||
->method('getCurrentOutOfOfficeData')
|
||||
->willReturn(null);
|
||||
$this->config->method('getUserValue')
|
||||
->with('user', 'dav', 'user_status_automation', 'no')
|
||||
->willReturn('yes');
|
||||
$this->time->method('getDateTime')
|
||||
->willReturn(new \DateTime('2023-02-24 13:58:24.479357', new \DateTimeZone('UTC')));
|
||||
$this->statusManager->expects($this->exactly(3))
|
||||
->method('revertUserStatus');
|
||||
$this->jobList->expects($this->once())
|
||||
->method('remove')
|
||||
->with(UserStatusAutomation::class, ['userId' => 'user']);
|
||||
$this->logger->expects(self::once())
|
||||
->method('debug');
|
||||
$this->logger->expects(self::once())
|
||||
->method('info');
|
||||
$automation = $this->getAutomationMock(['getAvailabilityFromPropertiesTable']);
|
||||
$automation->method('getAvailabilityFromPropertiesTable')
|
||||
->with('user')
|
||||
->willReturn(false);
|
||||
|
||||
self::invokePrivate($automation, 'run', [['userId' => 'user']]);
|
||||
}
|
||||
|
||||
public function testRunNoMoreAvailabilityDefined(): void {
|
||||
$this->config->method('getUserValue')
|
||||
->with('user', 'dav', 'user_status_automation', 'no')
|
||||
->willReturn('yes');
|
||||
public function testRunNoAvailabilityWithOOO(): void {
|
||||
$user = $this->createConfiguredMock(IUser::class, [
|
||||
'getUID' => 'user'
|
||||
]);
|
||||
$ooo = $this->createConfiguredMock(OutOfOfficeData::class, [
|
||||
'getShortMessage' => 'On Vacation',
|
||||
'getEndDate' => 123456,
|
||||
]);
|
||||
|
||||
$this->userManager->expects(self::once())
|
||||
->method('get')
|
||||
->willReturn($user);
|
||||
$this->coordinator->expects(self::once())
|
||||
->method('getCurrentOutOfOfficeData')
|
||||
->willReturn($ooo);
|
||||
$this->coordinator->expects(self::once())
|
||||
->method('isInEffect')
|
||||
->willReturn(true);
|
||||
$this->statusManager->expects($this->exactly(2))
|
||||
->method('revertUserStatus');
|
||||
$this->statusManager->expects(self::once())
|
||||
->method('setUserStatus')
|
||||
->with('user', IUserStatus::MESSAGE_VACATION, IUserStatus::DND, true, $ooo->getShortMessage());
|
||||
$this->config->expects(self::never())
|
||||
->method('getUserValue');
|
||||
$this->time->method('getDateTime')
|
||||
->willReturn(new \DateTime('2023-02-24 13:58:24.479357', new \DateTimeZone('UTC')));
|
||||
|
||||
$automation = $this->getAutomationMock(['getAvailabilityFromPropertiesTable']);
|
||||
$automation->method('getAvailabilityFromPropertiesTable')
|
||||
->with('user')
|
||||
->willReturn('BEGIN:VCALENDAR
|
||||
PRODID:Nextcloud DAV app
|
||||
BEGIN:VTIMEZONE
|
||||
TZID:Europe/Berlin
|
||||
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
|
||||
END:VAVAILABILITY
|
||||
END:VCALENDAR');
|
||||
|
||||
$this->statusManager->expects($this->once())
|
||||
->method('revertUserStatus')
|
||||
->with('user', IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND);
|
||||
|
||||
$this->jobList->expects($this->once())
|
||||
->method('remove')
|
||||
->with(UserStatusAutomation::class, ['userId' => 'user']);
|
||||
$this->jobList->expects($this->never())
|
||||
->method('remove');
|
||||
$this->logger->expects(self::exactly(2))
|
||||
->method('debug');
|
||||
$automation = $this->getAutomationMock([]);
|
||||
|
||||
self::invokePrivate($automation, 'run', [['userId' => 'user']]);
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -40,6 +40,7 @@ use OCP\User\Events\OutOfOfficeChangedEvent;
|
|||
use OCP\User\Events\OutOfOfficeClearedEvent;
|
||||
use OCP\User\Events\OutOfOfficeScheduledEvent;
|
||||
use OCP\User\IOutOfOfficeData;
|
||||
use OCP\UserStatus\IManager;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Sabre\DAV\Exception\NotFound;
|
||||
|
|
@ -55,6 +56,7 @@ class OutOfOfficeListenerTest extends TestCase {
|
|||
private IConfig|MockObject $appConfig;
|
||||
private LoggerInterface|MockObject $loggerInterface;
|
||||
private OutOfOfficeListener $listener;
|
||||
private IManager|MockObject $manager;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
|
@ -62,11 +64,13 @@ class OutOfOfficeListenerTest extends TestCase {
|
|||
$this->serverFactory = $this->createMock(ServerFactory::class);
|
||||
$this->appConfig = $this->createMock(IConfig::class);
|
||||
$this->loggerInterface = $this->createMock(LoggerInterface::class);
|
||||
$this->manager = $this->createMock(IManager::class);
|
||||
|
||||
$this->listener = new OutOfOfficeListener(
|
||||
$this->serverFactory,
|
||||
$this->appConfig,
|
||||
$this->loggerInterface,
|
||||
$this->manager
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -389,6 +393,8 @@ class OutOfOfficeListenerTest extends TestCase {
|
|||
->method('getPlugin')
|
||||
->with('caldav')
|
||||
->willReturn($caldavPlugin);
|
||||
$this->manager->expects(self::never())
|
||||
->method('revertUserStatus');
|
||||
$event = new OutOfOfficeClearedEvent($data);
|
||||
|
||||
$this->listener->handle($event);
|
||||
|
|
@ -417,6 +423,8 @@ class OutOfOfficeListenerTest extends TestCase {
|
|||
->method('getNodeForPath')
|
||||
->with('/home/calendar')
|
||||
->willThrowException(new NotFound('nope'));
|
||||
$this->manager->expects(self::never())
|
||||
->method('revertUserStatus');
|
||||
$event = new OutOfOfficeClearedEvent($data);
|
||||
|
||||
$this->listener->handle($event);
|
||||
|
|
@ -454,6 +462,8 @@ class OutOfOfficeListenerTest extends TestCase {
|
|||
->method('getChild')
|
||||
->with('personal-1')
|
||||
->willThrowException(new NotFound('nope'));
|
||||
$this->manager->expects(self::never())
|
||||
->method('revertUserStatus');
|
||||
$event = new OutOfOfficeClearedEvent($data);
|
||||
|
||||
$this->listener->handle($event);
|
||||
|
|
@ -495,6 +505,8 @@ class OutOfOfficeListenerTest extends TestCase {
|
|||
$calendar->expects(self::once())
|
||||
->method('getChild')
|
||||
->willThrowException(new NotFound());
|
||||
$this->manager->expects(self::never())
|
||||
->method('revertUserStatus');
|
||||
$event = new OutOfOfficeClearedEvent($data);
|
||||
|
||||
$this->listener->handle($event);
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ return array(
|
|||
'OCA\\UserStatus\\Exception\\InvalidStatusTypeException' => $baseDir . '/../lib/Exception/InvalidStatusTypeException.php',
|
||||
'OCA\\UserStatus\\Exception\\StatusMessageTooLongException' => $baseDir . '/../lib/Exception/StatusMessageTooLongException.php',
|
||||
'OCA\\UserStatus\\Listener\\BeforeTemplateRenderedListener' => $baseDir . '/../lib/Listener/BeforeTemplateRenderedListener.php',
|
||||
'OCA\\UserStatus\\Listener\\OutOfOfficeStatusListener' => $baseDir . '/../lib/Listener/OutOfOfficeStatusListener.php',
|
||||
'OCA\\UserStatus\\Listener\\UserDeletedListener' => $baseDir . '/../lib/Listener/UserDeletedListener.php',
|
||||
'OCA\\UserStatus\\Listener\\UserLiveStatusListener' => $baseDir . '/../lib/Listener/UserLiveStatusListener.php',
|
||||
'OCA\\UserStatus\\Migration\\Version0001Date20200602134824' => $baseDir . '/../lib/Migration/Version0001Date20200602134824.php',
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ class ComposerStaticInitUserStatus
|
|||
'OCA\\UserStatus\\Exception\\InvalidStatusTypeException' => __DIR__ . '/..' . '/../lib/Exception/InvalidStatusTypeException.php',
|
||||
'OCA\\UserStatus\\Exception\\StatusMessageTooLongException' => __DIR__ . '/..' . '/../lib/Exception/StatusMessageTooLongException.php',
|
||||
'OCA\\UserStatus\\Listener\\BeforeTemplateRenderedListener' => __DIR__ . '/..' . '/../lib/Listener/BeforeTemplateRenderedListener.php',
|
||||
'OCA\\UserStatus\\Listener\\OutOfOfficeStatusListener' => __DIR__ . '/..' . '/../lib/Listener/OutOfOfficeStatusListener.php',
|
||||
'OCA\\UserStatus\\Listener\\UserDeletedListener' => __DIR__ . '/..' . '/../lib/Listener/UserDeletedListener.php',
|
||||
'OCA\\UserStatus\\Listener\\UserLiveStatusListener' => __DIR__ . '/..' . '/../lib/Listener/UserLiveStatusListener.php',
|
||||
'OCA\\UserStatus\\Migration\\Version0001Date20200602134824' => __DIR__ . '/..' . '/../lib/Migration/Version0001Date20200602134824.php',
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ use OCA\UserStatus\Capabilities;
|
|||
use OCA\UserStatus\Connector\UserStatusProvider;
|
||||
use OCA\UserStatus\Dashboard\UserStatusWidget;
|
||||
use OCA\UserStatus\Listener\BeforeTemplateRenderedListener;
|
||||
use OCA\UserStatus\Listener\OutOfOfficeStatusListener;
|
||||
use OCA\UserStatus\Listener\UserDeletedListener;
|
||||
use OCA\UserStatus\Listener\UserLiveStatusListener;
|
||||
use OCP\AppFramework\App;
|
||||
|
|
@ -37,6 +38,9 @@ use OCP\AppFramework\Bootstrap\IBootstrap;
|
|||
use OCP\AppFramework\Bootstrap\IRegistrationContext;
|
||||
use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent;
|
||||
use OCP\IConfig;
|
||||
use OCP\User\Events\OutOfOfficeChangedEvent;
|
||||
use OCP\User\Events\OutOfOfficeClearedEvent;
|
||||
use OCP\User\Events\OutOfOfficeScheduledEvent;
|
||||
use OCP\User\Events\UserDeletedEvent;
|
||||
use OCP\User\Events\UserLiveStatusEvent;
|
||||
use OCP\UserStatus\IManager;
|
||||
|
|
@ -71,6 +75,9 @@ class Application extends App implements IBootstrap {
|
|||
$context->registerEventListener(UserDeletedEvent::class, UserDeletedListener::class);
|
||||
$context->registerEventListener(UserLiveStatusEvent::class, UserLiveStatusListener::class);
|
||||
$context->registerEventListener(BeforeTemplateRenderedEvent::class, BeforeTemplateRenderedListener::class);
|
||||
$context->registerEventListener(OutOfOfficeChangedEvent::class, OutOfOfficeStatusListener::class);
|
||||
$context->registerEventListener(OutOfOfficeScheduledEvent::class, OutOfOfficeStatusListener::class);
|
||||
$context->registerEventListener(OutOfOfficeClearedEvent::class, OutOfOfficeStatusListener::class);
|
||||
|
||||
$config = $this->getContainer()->query(IConfig::class);
|
||||
$shareeEnumeration = $config->getAppValue('core', 'shareapi_allow_share_dialog_user_enumeration', 'yes') === 'yes';
|
||||
|
|
|
|||
|
|
@ -57,8 +57,8 @@ class UserStatusProvider implements IProvider, ISettableProvider {
|
|||
return $userStatuses;
|
||||
}
|
||||
|
||||
public function setUserStatus(string $userId, string $messageId, string $status, bool $createBackup): void {
|
||||
$this->service->setUserStatus($userId, $status, $messageId, $createBackup);
|
||||
public function setUserStatus(string $userId, string $messageId, string $status, bool $createBackup, ?string $customMessage = null): void {
|
||||
$this->service->setUserStatus($userId, $status, $messageId, $createBackup, $customMessage);
|
||||
}
|
||||
|
||||
public function revertUserStatus(string $userId, string $messageId, string $status): void {
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ use OCP\AppFramework\Db\QBMapper;
|
|||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\UserStatus\IUserStatus;
|
||||
use Sabre\CalDAV\Schedule\Plugin;
|
||||
|
||||
/**
|
||||
* @template-extends QBMapper<UserStatus>
|
||||
|
|
@ -211,23 +210,4 @@ 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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
67
apps/user_status/lib/Listener/OutOfOfficeStatusListener.php
Normal file
67
apps/user_status/lib/Listener/OutOfOfficeStatusListener.php
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright Copyright (c) 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\UserStatus\Listener;
|
||||
|
||||
use OCA\DAV\BackgroundJob\UserStatusAutomation;
|
||||
use OCP\AppFramework\Utility\ITimeFactory;
|
||||
use OCP\BackgroundJob\IJobList;
|
||||
use OCP\EventDispatcher\Event;
|
||||
use OCP\EventDispatcher\IEventListener;
|
||||
use OCP\User\Events\OutOfOfficeChangedEvent;
|
||||
use OCP\User\Events\OutOfOfficeClearedEvent;
|
||||
use OCP\User\Events\OutOfOfficeScheduledEvent;
|
||||
use OCP\UserStatus\IManager;
|
||||
use OCP\UserStatus\IUserStatus;
|
||||
|
||||
/**
|
||||
* Class UserDeletedListener
|
||||
*
|
||||
* @template-implements IEventListener<OutOfOfficeScheduledEvent|OutOfOfficeChangedEvent|OutOfOfficeClearedEvent>
|
||||
*
|
||||
*/
|
||||
class OutOfOfficeStatusListener implements IEventListener {
|
||||
public function __construct(private IJobList $jobsList,
|
||||
private ITimeFactory $time,
|
||||
private IManager $manager) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function handle(Event $event): void {
|
||||
if($event instanceof OutOfOfficeClearedEvent) {
|
||||
$this->manager->revertUserStatus($event->getData()->getUser()->getUID(), IUserStatus::MESSAGE_VACATION, IUserStatus::DND);
|
||||
$this->jobsList->scheduleAfter(UserStatusAutomation::class, $this->time->getTime(), ['userId' => $event->getData()->getUser()->getUID()]);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($event instanceof OutOfOfficeScheduledEvent
|
||||
|| $event instanceof OutOfOfficeChangedEvent) {
|
||||
// This might be overwritten by the office hours automation, but that is ok. This is just in case no office hours are set
|
||||
$this->jobsList->scheduleAfter(UserStatusAutomation::class, $this->time->getTime(), ['userId' => $event->getData()->getUser()->getUID()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -202,6 +202,7 @@ class PredefinedStatusService {
|
|||
self::REMOTE_WORK,
|
||||
IUserStatus::MESSAGE_CALL,
|
||||
IUserStatus::MESSAGE_AVAILABILITY,
|
||||
IUserStatus::MESSAGE_VACATION,
|
||||
IUserStatus::MESSAGE_CALENDAR_BUSY,
|
||||
IUserStatus::MESSAGE_CALENDAR_BUSY_TENTATIVE,
|
||||
], true);
|
||||
|
|
|
|||
|
|
@ -137,37 +137,8 @@ class StatusService {
|
|||
* @return UserStatus
|
||||
* @throws DoesNotExistException
|
||||
*/
|
||||
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);
|
||||
public function findByUserId(string $userId):UserStatus {
|
||||
return $this->processStatus($this->mapper->findByUserId($userId));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -271,6 +242,7 @@ class StatusService {
|
|||
* @param string $status
|
||||
* @param string $messageId
|
||||
* @param bool $createBackup
|
||||
* @param string|null $customMessage
|
||||
* @throws InvalidStatusTypeException
|
||||
* @throws InvalidMessageIdException
|
||||
*/
|
||||
|
|
@ -278,7 +250,7 @@ class StatusService {
|
|||
string $status,
|
||||
string $messageId,
|
||||
bool $createBackup,
|
||||
string $customMessage = null): ?UserStatus {
|
||||
?string $customMessage = null): ?UserStatus {
|
||||
// Check if status-type is valid
|
||||
if (!in_array($status, self::PRIORITY_ORDERED_STATUSES, true)) {
|
||||
throw new InvalidStatusTypeException('Status-type "' . $status . '" is not supported');
|
||||
|
|
@ -313,13 +285,7 @@ class StatusService {
|
|||
$userStatus->setCustomIcon(null);
|
||||
$userStatus->setCustomMessage($customMessage);
|
||||
$userStatus->setClearAt(null);
|
||||
if ($this->predefinedStatusService->getTranslatedStatusForId($messageId) !== null
|
||||
|| ($customMessage !== null && $customMessage !== '')) {
|
||||
// Only track status message ID if there is one
|
||||
$userStatus->setStatusMessageTimestamp($this->timeFactory->now()->getTimestamp());
|
||||
} else {
|
||||
$userStatus->setStatusMessageTimestamp(0);
|
||||
}
|
||||
$userStatus->setStatusMessageTimestamp($this->timeFactory->now()->getTimestamp());
|
||||
|
||||
if ($userStatus->getId() !== null) {
|
||||
return $this->mapper->update($userStatus);
|
||||
|
|
@ -506,8 +472,14 @@ class StatusService {
|
|||
private function addDefaultMessage(UserStatus $status): void {
|
||||
// If the message is predefined, insert the translated message and icon
|
||||
$predefinedMessage = $this->predefinedStatusService->getDefaultStatusById($status->getMessageId());
|
||||
if ($predefinedMessage !== null) {
|
||||
if ($predefinedMessage === null) {
|
||||
return;
|
||||
}
|
||||
// If there is a custom message, don't overwrite it
|
||||
if(empty($status->getCustomMessage())) {
|
||||
$status->setCustomMessage($predefinedMessage['message']);
|
||||
}
|
||||
if(empty($status->getCustomIcon())) {
|
||||
$status->setCustomIcon($predefinedMessage['icon']);
|
||||
}
|
||||
}
|
||||
|
|
@ -588,8 +560,7 @@ class StatusService {
|
|||
}
|
||||
|
||||
/**
|
||||
* Calculate a users' status according to their availabilit settings and their calendar
|
||||
* events
|
||||
* Calculate a users' status according to their calendar events
|
||||
*
|
||||
* There are 4 predefined types of FBTYPE - 'FREE', 'BUSY', 'BUSY-UNAVAILABLE', 'BUSY-TENTATIVE',
|
||||
* but 'X-' properties are possible
|
||||
|
|
@ -598,11 +569,10 @@ class StatusService {
|
|||
*
|
||||
* 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
|
||||
* 'X-' properties and BUSY-UNAVAILABLE is not handled
|
||||
*
|
||||
* @param string $userId
|
||||
* @return CalendarStatus|null
|
||||
|
|
@ -612,9 +582,6 @@ class StatusService {
|
|||
if ($user === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$availability = $this->mapper->getAvailabilityFromPropertiesTable($userId);
|
||||
|
||||
return $this->calendarStatusService->processCalendarAvailability($user, $availability);
|
||||
return $this->calendarStatusService->processCalendarAvailability($user);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -845,15 +845,13 @@ class StatusServiceTest extends TestCase {
|
|||
->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 {
|
||||
public function testCalendarAvailabilityNoStatus(): void {
|
||||
$user = $this->createConfiguredMock(IUser::class, [
|
||||
'getUID' => 'admin',
|
||||
'getEMailAddress' => 'test@test.com',
|
||||
|
|
@ -863,299 +861,11 @@ class StatusServiceTest extends TestCase {
|
|||
->method('get')
|
||||
->with($user->getUID())
|
||||
->willReturn($user);
|
||||
$this->mapper->expects(self::once())
|
||||
->method('getAvailabilityFromPropertiesTable')
|
||||
->willReturn('');
|
||||
$this->calendarStatusService->expects(self::once())
|
||||
->method('processCalendarAvailability')
|
||||
->with($user, '')
|
||||
->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'));
|
||||
}
|
||||
|
||||
public function testSetStatusWithoutMessage(): void {
|
||||
$this->predefinedStatusService->expects(self::once())
|
||||
->method('isValidId')
|
||||
->with(IUserStatus::MESSAGE_AVAILABILITY)
|
||||
->willReturn(true);
|
||||
$this->timeFactory
|
||||
->method('getTime')
|
||||
->willReturn(1234);
|
||||
$status = new UserStatus();
|
||||
$status->setUserId('admin');
|
||||
$status->setStatusTimestamp(1234);
|
||||
$status->setIsUserDefined(true);
|
||||
$status->setStatus(IUserStatus::DND);
|
||||
$status->setIsBackup(false);
|
||||
$status->setMessageId(IUserStatus::MESSAGE_AVAILABILITY);
|
||||
$this->mapper->expects(self::once())
|
||||
->method('insert')
|
||||
->with($this->equalTo($status))
|
||||
->willReturnArgument(0);
|
||||
|
||||
$result = $this->service->setUserStatus(
|
||||
'admin',
|
||||
IUserStatus::DND,
|
||||
IUserStatus::MESSAGE_AVAILABILITY,
|
||||
true,
|
||||
);
|
||||
|
||||
self::assertNotNull($result);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,8 +29,7 @@ namespace OC\User;
|
|||
use JsonException;
|
||||
use OCA\DAV\AppInfo\Application;
|
||||
use OCA\DAV\CalDAV\TimezoneService;
|
||||
use OCA\DAV\Db\AbsenceMapper;
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use OCA\DAV\Service\AbsenceService;
|
||||
use OCP\ICache;
|
||||
use OCP\ICacheFactory;
|
||||
use OCP\IConfig;
|
||||
|
|
@ -44,8 +43,8 @@ class AvailabilityCoordinator implements IAvailabilityCoordinator {
|
|||
|
||||
public function __construct(
|
||||
ICacheFactory $cacheFactory,
|
||||
private AbsenceMapper $absenceMapper,
|
||||
private IConfig $config,
|
||||
private AbsenceService $absenceService,
|
||||
private LoggerInterface $logger,
|
||||
private TimezoneService $timezoneService,
|
||||
) {
|
||||
|
|
@ -53,11 +52,7 @@ class AvailabilityCoordinator implements IAvailabilityCoordinator {
|
|||
}
|
||||
|
||||
public function isEnabled(): bool {
|
||||
return $this->config->getAppValue(
|
||||
Application::APP_ID,
|
||||
'hide_absence_settings',
|
||||
'no',
|
||||
) === 'no';
|
||||
return $this->config->getAppValue(Application::APP_ID, 'hide_absence_settings', 'no') === 'no';
|
||||
}
|
||||
|
||||
private function getCachedOutOfOfficeData(IUser $user): ?OutOfOfficeData {
|
||||
|
|
@ -106,22 +101,39 @@ class AvailabilityCoordinator implements IAvailabilityCoordinator {
|
|||
}
|
||||
|
||||
public function getCurrentOutOfOfficeData(IUser $user): ?IOutOfOfficeData {
|
||||
$cachedData = $this->getCachedOutOfOfficeData($user);
|
||||
if ($cachedData !== null) {
|
||||
return $cachedData;
|
||||
$timezone = $this->getCachedTimezone($user->getUID());
|
||||
if ($timezone === null) {
|
||||
$timezone = $this->timezoneService->getUserTimezone($user->getUID()) ?? $this->timezoneService->getDefaultTimezone();
|
||||
$this->setCachedTimezone($user->getUID(), $timezone);
|
||||
}
|
||||
|
||||
try {
|
||||
$absenceData = $this->absenceMapper->findByUserId($user->getUID());
|
||||
} catch (DoesNotExistException $e) {
|
||||
return null;
|
||||
$data = $this->getCachedOutOfOfficeData($user);
|
||||
if ($data === null) {
|
||||
$absenceData = $this->absenceService->getAbsence($user->getUID());
|
||||
if ($absenceData === null) {
|
||||
return null;
|
||||
}
|
||||
$data = $absenceData->toOutOufOfficeData($user, $timezone);
|
||||
}
|
||||
|
||||
$data = $absenceData->toOutOufOfficeData(
|
||||
$user,
|
||||
$this->timezoneService->getUserTimezone($user->getUID()) ?? $this->timezoneService->getDefaultTimezone(),
|
||||
);
|
||||
$this->setCachedOutOfOfficeData($data);
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function getCachedTimezone(string $userId): ?string {
|
||||
return $this->cache->get($userId . '_timezone') ?? null;
|
||||
}
|
||||
|
||||
private function setCachedTimezone(string $userId, string $timezone): void {
|
||||
$this->cache->set($userId . '_timezone', $timezone, 3600);
|
||||
}
|
||||
|
||||
public function clearCache(string $userId): void {
|
||||
$this->cache->set($userId, null, 300);
|
||||
$this->cache->set($userId . '_timezone', null, 3600);
|
||||
}
|
||||
|
||||
public function isInEffect(IOutOfOfficeData $data): bool {
|
||||
return $this->absenceService->isInEffect($data);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,8 +39,9 @@ interface ISettableProvider extends IProvider {
|
|||
* @param string $messageId The new message id.
|
||||
* @param string $status The new status.
|
||||
* @param bool $createBackup If true, this will store the old status so that it is possible to revert it later (e.g. after a call).
|
||||
* @param string|null $customMessage
|
||||
*/
|
||||
public function setUserStatus(string $userId, string $messageId, string $status, bool $createBackup): void;
|
||||
public function setUserStatus(string $userId, string $messageId, string $status, bool $createBackup, ?string $customMessage = null): void;
|
||||
|
||||
/**
|
||||
* Revert an automatically set user status. For example after leaving a call,
|
||||
|
|
|
|||
|
|
@ -104,13 +104,13 @@ class Manager implements IManager {
|
|||
$this->provider = $provider;
|
||||
}
|
||||
|
||||
public function setUserStatus(string $userId, string $messageId, string $status, bool $createBackup = false): void {
|
||||
public function setUserStatus(string $userId, string $messageId, string $status, bool $createBackup = false, ?string $customMessage = null): void {
|
||||
$this->setupProvider();
|
||||
if (!$this->provider || !($this->provider instanceof ISettableProvider)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->provider->setUserStatus($userId, $messageId, $status, $createBackup);
|
||||
$this->provider->setUserStatus($userId, $messageId, $status, $createBackup, $customMessage);
|
||||
}
|
||||
|
||||
public function revertUserStatus(string $userId, string $messageId, string $status): void {
|
||||
|
|
|
|||
|
|
@ -48,4 +48,20 @@ interface IAvailabilityCoordinator {
|
|||
* @since 28.0.0
|
||||
*/
|
||||
public function getCurrentOutOfOfficeData(IUser $user): ?IOutOfOfficeData;
|
||||
|
||||
/**
|
||||
* Reset the absence cache to null
|
||||
*
|
||||
* @since 28.0.0
|
||||
*/
|
||||
public function clearCache(string $userId): void;
|
||||
|
||||
/**
|
||||
* Is the absence in effect at this moment
|
||||
*
|
||||
* @param IOutOfOfficeData $data
|
||||
* @return bool
|
||||
* @since 28.0.0
|
||||
*/
|
||||
public function isInEffect(IOutOfOfficeData $data): bool;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,9 +52,11 @@ interface IManager {
|
|||
* @param string $messageId The id of the predefined message.
|
||||
* @param string $status The status to assign
|
||||
* @param bool $createBackup If true, this will store the old status so that it is possible to revert it later (e.g. after a call).
|
||||
* @param string|null $customMessage
|
||||
* @since 23.0.0
|
||||
* @since 28.0.0 Optional parameter $customMessage was added
|
||||
*/
|
||||
public function setUserStatus(string $userId, string $messageId, string $status, bool $createBackup = false): void;
|
||||
public function setUserStatus(string $userId, string $messageId, string $status, bool $createBackup = false, ?string $customMessage = null): void;
|
||||
|
||||
/**
|
||||
* Revert an automatically set user status. For example after leaving a call,
|
||||
|
|
|
|||
|
|
@ -81,6 +81,12 @@ interface IUserStatus {
|
|||
*/
|
||||
public const MESSAGE_AVAILABILITY = 'availability';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
* @since 28.0.0
|
||||
*/
|
||||
public const MESSAGE_VACATION = 'vacationing';
|
||||
|
||||
/**
|
||||
* @var string
|
||||
* @since 28.0.0
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ use OC\User\AvailabilityCoordinator;
|
|||
use OC\User\OutOfOfficeData;
|
||||
use OCA\DAV\CalDAV\TimezoneService;
|
||||
use OCA\DAV\Db\Absence;
|
||||
use OCA\DAV\Db\AbsenceMapper;
|
||||
use OCA\DAV\Service\AbsenceService;
|
||||
use OCP\ICache;
|
||||
use OCP\ICacheFactory;
|
||||
use OCP\IConfig;
|
||||
|
|
@ -44,7 +44,7 @@ class AvailabilityCoordinatorTest extends TestCase {
|
|||
private ICacheFactory $cacheFactory;
|
||||
private ICache $cache;
|
||||
private IConfig|MockObject $config;
|
||||
private AbsenceMapper $absenceMapper;
|
||||
private AbsenceService $absenceService;
|
||||
private LoggerInterface $logger;
|
||||
private MockObject|TimezoneService $timezoneService;
|
||||
|
||||
|
|
@ -53,7 +53,7 @@ class AvailabilityCoordinatorTest extends TestCase {
|
|||
|
||||
$this->cacheFactory = $this->createMock(ICacheFactory::class);
|
||||
$this->cache = $this->createMock(ICache::class);
|
||||
$this->absenceMapper = $this->createMock(AbsenceMapper::class);
|
||||
$this->absenceService = $this->createMock(AbsenceService::class);
|
||||
$this->config = $this->createMock(IConfig::class);
|
||||
$this->logger = $this->createMock(LoggerInterface::class);
|
||||
$this->timezoneService = $this->createMock(TimezoneService::class);
|
||||
|
|
@ -64,8 +64,8 @@ class AvailabilityCoordinatorTest extends TestCase {
|
|||
|
||||
$this->availabilityCoordinator = new AvailabilityCoordinator(
|
||||
$this->cacheFactory,
|
||||
$this->absenceMapper,
|
||||
$this->config,
|
||||
$this->absenceService,
|
||||
$this->logger,
|
||||
$this->timezoneService,
|
||||
);
|
||||
|
|
@ -82,7 +82,7 @@ class AvailabilityCoordinatorTest extends TestCase {
|
|||
self::assertTrue($isEnabled);
|
||||
}
|
||||
|
||||
public function testGetOutOfOfficeData(): void {
|
||||
public function testGetOutOfOfficeDataInEffect(): void {
|
||||
$absence = new Absence();
|
||||
$absence->setId(420);
|
||||
$absence->setUserId('user');
|
||||
|
|
@ -96,17 +96,17 @@ class AvailabilityCoordinatorTest extends TestCase {
|
|||
$user->method('getUID')
|
||||
->willReturn('user');
|
||||
|
||||
$this->cache->expects(self::once())
|
||||
$this->cache->expects(self::exactly(2))
|
||||
->method('get')
|
||||
->with('user')
|
||||
->willReturn(null);
|
||||
$this->absenceMapper->expects(self::once())
|
||||
->method('findByUserId')
|
||||
->with('user')
|
||||
->willReturnOnConsecutiveCalls(null, null);
|
||||
$this->absenceService->expects(self::once())
|
||||
->method('getAbsence')
|
||||
->with($user->getUID())
|
||||
->willReturn($absence);
|
||||
$this->cache->expects(self::once())
|
||||
$this->cache->expects(self::exactly(2))
|
||||
->method('set')
|
||||
->with('user', '{"id":"420","startDate":1696111200,"endDate":1696802340,"shortMessage":"Vacation","message":"On vacation"}', 300);
|
||||
->withConsecutive([$user->getUID() . '_timezone', 'Europe/Berlin', 3600],
|
||||
[$user->getUID(), '{"id":"420","startDate":1696111200,"endDate":1696802340,"shortMessage":"Vacation","message":"On vacation"}', 300]);
|
||||
|
||||
$expected = new OutOfOfficeData(
|
||||
'420',
|
||||
|
|
@ -120,25 +120,32 @@ class AvailabilityCoordinatorTest extends TestCase {
|
|||
self::assertEquals($expected, $actual);
|
||||
}
|
||||
|
||||
public function testGetOutOfOfficeDataWithCachedData(): void {
|
||||
public function testGetOutOfOfficeDataCachedAll(): void {
|
||||
$absence = new Absence();
|
||||
$absence->setId(420);
|
||||
$absence->setUserId('user');
|
||||
$absence->setFirstDay('2023-10-01');
|
||||
$absence->setLastDay('2023-10-08');
|
||||
$absence->setStatus('Vacation');
|
||||
$absence->setMessage('On vacation');
|
||||
|
||||
$user = $this->createMock(IUser::class);
|
||||
$user->method('getUID')
|
||||
->willReturn('user');
|
||||
|
||||
$this->cache->expects(self::once())
|
||||
$this->cache->expects(self::exactly(2))
|
||||
->method('get')
|
||||
->with('user')
|
||||
->willReturn('{"id":"420","startDate":1696118400,"endDate":1696723200,"shortMessage":"Vacation","message":"On vacation"}');
|
||||
$this->absenceMapper->expects(self::never())
|
||||
->method('findByUserId');
|
||||
$this->cache->expects(self::never())
|
||||
->willReturnOnConsecutiveCalls('UTC', '{"id":"420","startDate":1696118400,"endDate":1696809540,"shortMessage":"Vacation","message":"On vacation"}');
|
||||
$this->absenceService->expects(self::never())
|
||||
->method('getAbsence');
|
||||
$this->cache->expects(self::exactly(1))
|
||||
->method('set');
|
||||
|
||||
$expected = new OutOfOfficeData(
|
||||
'420',
|
||||
$user,
|
||||
1696118400,
|
||||
1696723200,
|
||||
1696809540,
|
||||
'Vacation',
|
||||
'On vacation',
|
||||
);
|
||||
|
|
@ -146,6 +153,32 @@ class AvailabilityCoordinatorTest extends TestCase {
|
|||
self::assertEquals($expected, $actual);
|
||||
}
|
||||
|
||||
public function testGetOutOfOfficeDataNoData(): void {
|
||||
$absence = new Absence();
|
||||
$absence->setId(420);
|
||||
$absence->setUserId('user');
|
||||
$absence->setFirstDay('2023-10-01');
|
||||
$absence->setLastDay('2023-10-08');
|
||||
$absence->setStatus('Vacation');
|
||||
$absence->setMessage('On vacation');
|
||||
|
||||
$user = $this->createMock(IUser::class);
|
||||
$user->method('getUID')
|
||||
->willReturn('user');
|
||||
|
||||
$this->cache->expects(self::exactly(2))
|
||||
->method('get')
|
||||
->willReturnOnConsecutiveCalls('UTC', null);
|
||||
$this->absenceService->expects(self::once())
|
||||
->method('getAbsence')
|
||||
->willReturn(null);
|
||||
$this->cache->expects(self::never())
|
||||
->method('set');
|
||||
|
||||
$actual = $this->availabilityCoordinator->getCurrentOutOfOfficeData($user);
|
||||
self::assertNull($actual);
|
||||
}
|
||||
|
||||
public function testGetOutOfOfficeDataWithInvalidCachedData(): void {
|
||||
$absence = new Absence();
|
||||
$absence->setId(420);
|
||||
|
|
@ -160,23 +193,22 @@ class AvailabilityCoordinatorTest extends TestCase {
|
|||
$user->method('getUID')
|
||||
->willReturn('user');
|
||||
|
||||
$this->cache->expects(self::once())
|
||||
$this->cache->expects(self::exactly(2))
|
||||
->method('get')
|
||||
->with('user')
|
||||
->willReturn('{"id":"420",}');
|
||||
$this->absenceMapper->expects(self::once())
|
||||
->method('findByUserId')
|
||||
->willReturnOnConsecutiveCalls('UTC', '{"id":"420",}');
|
||||
$this->absenceService->expects(self::once())
|
||||
->method('getAbsence')
|
||||
->with('user')
|
||||
->willReturn($absence);
|
||||
$this->cache->expects(self::once())
|
||||
->method('set')
|
||||
->with('user', '{"id":"420","startDate":1696111200,"endDate":1696802340,"shortMessage":"Vacation","message":"On vacation"}', 300);
|
||||
->with('user', '{"id":"420","startDate":1696118400,"endDate":1696809540,"shortMessage":"Vacation","message":"On vacation"}', 300);
|
||||
|
||||
$expected = new OutOfOfficeData(
|
||||
'420',
|
||||
$user,
|
||||
1696111200,
|
||||
1696802340,
|
||||
1696118400,
|
||||
1696809540,
|
||||
'Vacation',
|
||||
'On vacation',
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue