Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[stable28] enh(userstatus): add OOO automation and remove calendar automation #41798

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 112 additions & 57 deletions apps/dav/lib/BackgroundJob/UserStatusAutomation.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
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;
Expand All @@ -39,24 +43,15 @@
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.
Expand All @@ -74,21 +69,74 @@ protected function run($argument) {
}

$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;
}

$this->processAvailability($property, $user->getUID(), $hasDndForOfficeHours);
}

protected function setLastRunToNextToggleTime(string $userId, int $timestamp): void {
$query = $this->connection->getQueryBuilder();

$query->update('jobs')
->set('last_run', $query->createNamedParameter($timestamp, IQueryBuilder::PARAM_INT))
->where($query->expr()->eq('id', $query->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT)));
$query->executeStatement();

$this->logger->debug('Updated user status automation last_run to ' . $timestamp . ' for user ' . $userId);
}

/**
* @param string $userId
* @return false|string
*/
protected function getAvailabilityFromPropertiesTable(string $userId) {
$propertyPath = 'calendars/' . $userId . '/inbox';
$propertyName = '{' . Plugin::NS_CALDAV . '}calendar-availability';

$query = $this->connection->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;
}

/**
* @param string $property
* @param $userId
* @param $argument
* @return void
*/
private function processAvailability(string $property, string $userId, bool $hasDndForOfficeHours): void {
$isCurrentlyAvailable = false;
$nextPotentialToggles = [];

Expand Down Expand Up @@ -117,7 +165,7 @@ protected function run($argument) {
$effectiveEnd = \DateTime::createFromImmutable($originalEnd)->sub(new \DateInterval('P7D'));

try {
$it = new RRuleIterator((string) $available->RRULE, $effectiveStart);
$it = new RRuleIterator((string)$available->RRULE, $effectiveStart);
$it->fastForward($lastMidnight);

$startToday = $it->current();
Expand Down Expand Up @@ -148,7 +196,7 @@ protected function run($argument) {

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->jobList->remove(self::class, ['userId' => $userId]);
$this->manager->revertUserStatus($userId, IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND);
return;
}
Expand All @@ -159,46 +207,53 @@ protected function run($argument) {
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');
return;
}
$this->logger->debug('User status automation ran');
}

protected function setLastRunToNextToggleTime(string $userId, int $timestamp): void {
$query = $this->connection->getQueryBuilder();

$query->update('jobs')
->set('last_run', $query->createNamedParameter($timestamp, IQueryBuilder::PARAM_INT))
->where($query->expr()->eq('id', $query->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT)));
$query->executeStatement();
if(!$hasDndForOfficeHours) {
// Office hours are not set to DND, so there is nothing to do.
return;
}

$this->logger->debug('Updated user status automation last_run to ' . $timestamp . ' for user ' . $userId);
$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');
}

/**
* @param string $userId
* @return false|string
*/
protected function getAvailabilityFromPropertiesTable(string $userId) {
$propertyPath = 'calendars/' . $userId . '/inbox';
$propertyName = '{' . Plugin::NS_CALDAV . '}calendar-availability';
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;
}

$query = $this->connection->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);
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);

$result = $query->executeQuery();
$property = $result->fetchOne();
$result->closeCursor();
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;
}

return $property;
$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;
}
}
17 changes: 15 additions & 2 deletions apps/dav/lib/CalDAV/Status/Status.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -54,5 +53,19 @@ public function setCustomMessage(?string $customMessage): void {
$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;
}
}
24 changes: 5 additions & 19 deletions apps/dav/lib/CalDAV/Status/StatusService.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@
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,
Expand All @@ -76,7 +75,7 @@ public function __construct(private ITimeFactory $timeFactory,
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) {
Expand Down Expand Up @@ -160,8 +159,7 @@ public function processCalendarAvailability(User $user, ?string $availability):
}

// @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;
}

Expand All @@ -181,15 +179,6 @@ public function processCalendarAvailability(User $user, ?string $availability):
$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)) {
Expand All @@ -200,9 +189,8 @@ public function processCalendarAvailability(User $user, ?string $availability):
$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 */
Expand All @@ -220,12 +208,10 @@ public function processCalendarAvailability(User $user, ?string $availability):
}
$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;
}
Expand Down
4 changes: 4 additions & 0 deletions apps/dav/lib/Controller/AvailabilitySettingsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,14 @@
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);
}
Expand Down Expand Up @@ -75,6 +77,7 @@ public function updateAbsence(
$status,
$message,
);
$this->coordinator->clearCache($user->getUID());
return new JSONResponse($absence);
}

Expand All @@ -89,6 +92,7 @@ public function clearAbsence(): Response {
}

$this->absenceService->clearAbsence($user);
$this->coordinator->clearCache($user->getUID());
return new JSONResponse([]);
}

Expand Down
4 changes: 2 additions & 2 deletions apps/dav/lib/Db/Absence.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@
namespace OCA\DAV\Db;

use DateTime;
use DateTimeZone;
use Exception;
use InvalidArgumentException;
use JsonSerializable;
Expand Down Expand Up @@ -58,6 +57,7 @@ class Absence extends Entity implements JsonSerializable {
protected string $lastDay = '';

protected string $status = '';

protected string $message = '';

public function __construct() {
Expand All @@ -76,7 +76,7 @@ public function toOutOufOfficeData(IUser $user, string $timezone): IOutOfOfficeD
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);
Expand Down
Loading
Loading