2022-05-23 17:58:47 +02:00
< ? php
declare ( strict_types = 1 );
/**
2024-05-27 17:39:07 +02:00
* SPDX - FileCopyrightText : 2022 Nextcloud GmbH and Nextcloud contributors
* SPDX - License - Identifier : AGPL - 3.0 - or - later
2022-05-23 17:58:47 +02:00
*/
namespace OCA\DAV\BackgroundJob ;
use OCA\DAV\CalDAV\Schedule\Plugin ;
use OCP\AppFramework\Utility\ITimeFactory ;
use OCP\BackgroundJob\IJobList ;
use OCP\BackgroundJob\TimedJob ;
2022-05-24 14:11:42 +02:00
use OCP\DB\QueryBuilder\IQueryBuilder ;
2022-05-23 17:58:47 +02:00
use OCP\IConfig ;
use OCP\IDBConnection ;
2023-11-24 01:49:30 +01:00
use OCP\IUser ;
use OCP\IUserManager ;
use OCP\User\IAvailabilityCoordinator ;
use OCP\User\IOutOfOfficeData ;
2022-05-24 14:40:59 +02:00
use OCP\UserStatus\IManager ;
use OCP\UserStatus\IUserStatus ;
2022-05-23 17:58:47 +02:00
use Psr\Log\LoggerInterface ;
2022-05-24 14:11:42 +02:00
use Sabre\VObject\Component\Available ;
use Sabre\VObject\Component\VAvailability ;
2022-05-23 17:58:47 +02:00
use Sabre\VObject\Reader ;
2022-05-24 14:11:42 +02:00
use Sabre\VObject\Recur\RRuleIterator ;
2022-05-23 17:58:47 +02:00
class UserStatusAutomation extends TimedJob {
2023-11-24 01:49:30 +01:00
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 ,
) {
2022-05-23 17:58:47 +02:00
parent :: __construct ( $timeFactory );
2022-05-24 14:11:42 +02:00
// 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.
$this -> setInterval ( 0 );
2022-05-23 17:58:47 +02:00
}
/**
* @ inheritDoc
*/
protected function run ( $argument ) {
if ( ! isset ( $argument [ 'userId' ])) {
$this -> jobList -> remove ( self :: class , $argument );
$this -> logger -> info ( 'Removing invalid ' . self :: class . ' background job' );
return ;
}
$userId = $argument [ 'userId' ];
2023-11-24 01:49:30 +01:00
$user = $this -> userManager -> get ( $userId );
if ( $user === null ) {
return ;
}
$ooo = $this -> coordinator -> getCurrentOutOfOfficeData ( $user );
$continue = $this -> processOutOfOfficeData ( $user , $ooo );
if ( $continue === false ) {
2022-05-23 17:58:47 +02:00
return ;
}
2022-05-24 14:11:42 +02:00
$property = $this -> getAvailabilityFromPropertiesTable ( $userId );
2023-11-24 01:49:30 +01:00
$hasDndForOfficeHours = $this -> config -> getUserValue ( $userId , 'dav' , 'user_status_automation' , 'no' ) === 'yes' ;
2022-05-24 14:11:42 +02:00
if ( ! $property ) {
2023-11-24 01:49:30 +01:00
// 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' );
2022-05-24 14:11:42 +02:00
$this -> jobList -> remove ( self :: class , $argument );
2023-11-24 01:49:30 +01:00
$this -> manager -> revertUserStatus ( $user -> getUID (), IUserStatus :: MESSAGE_AVAILABILITY , IUserStatus :: DND );
2023-12-15 11:07:09 +01:00
$this -> manager -> revertUserStatus ( $user -> getUID (), IUserStatus :: MESSAGE_OUT_OF_OFFICE , IUserStatus :: DND );
2022-05-24 14:11:42 +02:00
return ;
}
2023-11-24 01:49:30 +01:00
$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 {
2022-05-24 14:11:42 +02:00
$isCurrentlyAvailable = false ;
$nextPotentialToggles = [];
2023-02-24 15:22:09 +01:00
$now = $this -> time -> getDateTime ();
2022-05-24 14:11:42 +02:00
$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' ) {
2023-02-24 15:22:09 +01:00
/** @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' ));
2022-05-24 14:11:42 +02:00
try {
2023-11-24 01:49:30 +01:00
$it = new RRuleIterator (( string ) $available -> RRULE , $effectiveStart );
2022-05-24 14:11:42 +02:00
$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 ]);
}
}
}
}
2023-03-10 09:40:36 +01:00
if ( empty ( $nextPotentialToggles )) {
$this -> logger -> info ( 'Removing ' . self :: class . ' background job for user "' . $userId . '" because the user has no valid availability rules set' );
2023-11-24 01:49:30 +01:00
$this -> jobList -> remove ( self :: class , [ 'userId' => $userId ]);
2023-03-10 09:40:36 +01:00
$this -> manager -> revertUserStatus ( $userId , IUserStatus :: MESSAGE_AVAILABILITY , IUserStatus :: DND );
return ;
}
2022-05-24 14:11:42 +02:00
$nextAutomaticToggle = min ( $nextPotentialToggles );
$this -> setLastRunToNextToggleTime ( $userId , $nextAutomaticToggle - 1 );
2022-05-24 14:40:59 +02:00
if ( $isCurrentlyAvailable ) {
2023-02-24 15:22:09 +01:00
$this -> logger -> debug ( 'User is currently available, reverting DND status if applicable' );
2022-05-24 14:40:59 +02:00
$this -> manager -> revertUserStatus ( $userId , IUserStatus :: MESSAGE_AVAILABILITY , IUserStatus :: DND );
2023-11-24 01:49:30 +01:00
$this -> logger -> debug ( 'User status automation ran' );
return ;
2022-05-24 14:40:59 +02:00
}
2022-05-24 14:11:42 +02:00
2023-11-24 01:49:30 +01:00
if ( ! $hasDndForOfficeHours ) {
// Office hours are not set to DND, so there is nothing to do.
return ;
}
2022-05-24 14:11:42 +02:00
2024-06-24 16:28:43 +02:00
$this -> logger -> debug ( 'User is currently NOT available, reverting call and meeting status if applicable and then setting DND' );
2023-11-24 01:49:30 +01:00
$this -> manager -> setUserStatus ( $userId , IUserStatus :: MESSAGE_AVAILABILITY , IUserStatus :: DND , true );
$this -> logger -> debug ( 'User status automation ran' );
2022-05-24 14:11:42 +02:00
}
2023-11-24 01:49:30 +01:00
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' );
2023-12-15 11:07:09 +01:00
$this -> manager -> revertUserStatus ( $user -> getUID (), IUserStatus :: MESSAGE_OUT_OF_OFFICE , IUserStatus :: DND );
2023-11-24 01:49:30 +01:00
// We need to also run the availability automation
return true ;
}
2022-05-23 17:58:47 +02:00
2023-11-24 01:49:30 +01:00
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' );
2023-12-15 11:07:09 +01:00
$this -> manager -> revertUserStatus ( $user -> getUID (), IUserStatus :: MESSAGE_OUT_OF_OFFICE , IUserStatus :: DND );
2022-05-23 17:58:47 +02:00
2023-11-24 01:49:30 +01:00
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 ;
}
2022-05-23 17:58:47 +02:00
2023-11-24 01:49:30 +01:00
$this -> logger -> debug ( 'User is currently in an OOO period, reverting other automated status and setting OOO DND status' );
2023-12-15 11:07:09 +01:00
$this -> manager -> setUserStatus ( $user -> getUID (), IUserStatus :: MESSAGE_OUT_OF_OFFICE , IUserStatus :: DND , true , $ooo -> getShortMessage ());
2024-06-24 16:28:43 +02:00
2023-11-24 01:49:30 +01:00
// 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 ;
2022-05-23 17:58:47 +02:00
}
}