Skip to content

Commit

Permalink
[stable28] enh(userstatus): add OOO automation and remove calendar au…
Browse files Browse the repository at this point in the history
…tomation

Signed-off-by: Anna Larch <anna@nextcloud.com>
  • Loading branch information
miaulalala committed Nov 28, 2023
1 parent e7b1d1f commit 51806ce
Show file tree
Hide file tree
Showing 27 changed files with 554 additions and 1,431 deletions.
169 changes: 112 additions & 57 deletions apps/dav/lib/BackgroundJob/UserStatusAutomation.php
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
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
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
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
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

0 comments on commit 51806ce

Please sign in to comment.