Skip to content

Commit

Permalink
fixup! feat(user status): automate user status for events
Browse files Browse the repository at this point in the history
  • Loading branch information
miaulalala committed Oct 3, 2023
1 parent b334893 commit f4cfa4d
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 33 deletions.
2 changes: 1 addition & 1 deletion apps/user_status/lib/Db/UserStatusMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ public function getAvailabilityFromPropertiesTable(string $userId): ?string {
$propertyPath = 'calendars/' . $userId . '/inbox';
$propertyName = '{' . Plugin::NS_CALDAV . '}calendar-availability';

$query = $this->connection->getQueryBuilder();
$query = $this->db->getQueryBuilder();
$query->select('propertyvalue')
->from('properties')
->where($query->expr()->eq('userid', $query->createNamedParameter($userId)))
Expand Down
2 changes: 2 additions & 0 deletions apps/user_status/lib/Service/PredefinedStatusService.php
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,8 @@ public function isValidId(string $id): bool {
self::REMOTE_WORK,
IUserStatus::MESSAGE_CALL,
IUserStatus::MESSAGE_AVAILABILITY,
IUserStatus::MESSAGE_CALENDAR_BUSY,
IUserStatus::MESSAGE_CALENDAR_BUSY_TENTATIVE,
], true);
}
}
90 changes: 58 additions & 32 deletions apps/user_status/lib/Service/StatusService.php
Original file line number Diff line number Diff line change
Expand Up @@ -563,15 +563,34 @@ public function revertMultipleUserStatus(array $userIds, string $messageId): voi
$this->mapper->restoreBackupStatuses($restoreIds);
}

private function processCalendarAvailability(string $userId) {
/**
* Calculate a users' status according to their availabilit settings and their calendar
* events
*
* There are 4 predefined types of FBTYPE - 'FREE', 'BUSY', 'BUSY-UNAVAILABLE', 'BUSY-TENTATIVE',
* but 'X-' properties are possible
* @link https://icalendar.org/iCalendar-RFC-5545/3-2-9-free-busy-time-type.html
*
* The status will be changed for types
* - 'BUSY'
* - 'BUSY-UNAVAILABLE' (ex.: when a VAVILABILITY setting is in effect)
* - 'BUSY-TENTATIVE' (ex.: an event has been accepted tentatively)
* and all FREEBUSY components without a type (implicitly a 'BUSY' status)
*
* 'X-' properties are not handled for now
*
* @param string $userId
* @return void
*/
private function processCalendarAvailability(string $userId): void {
$user = $this->userManager->get($userId);
if($user === null) {
return false;
return;
}

$email = $user->getEMailAddress();
if($email === null) {
return false;
return;
}

$server = new InvitationResponseServer();
Expand All @@ -598,7 +617,7 @@ private function processCalendarAvailability(string $userId) {
);

if (!count($result) || !isset($result[0][200][$caldavNS.'schedule-inbox-URL'])) {
return false;
return;
}

$inboxUrl = $result[0][200][$caldavNS.'schedule-inbox-URL']->getHref();
Expand All @@ -607,13 +626,13 @@ private function processCalendarAvailability(string $userId) {
try {
$aclPlugin->checkPrivileges($inboxUrl, $caldavNS.'schedule-query-freebusy');
} catch (NeedPrivileges | NotAuthenticated $exception) {
return false;
return;
}

$calendarTimeZone = new DateTimeZone('UTC');
$calendars = $this->calendarManager->getCalendarsForPrincipal('principals/users/' . $userId);
if(empty($calendars)) {
return false;
return;
}

$query = new CalendarQuery('principals/users/' . $userId);
Expand All @@ -624,7 +643,6 @@ private function processCalendarAvailability(string $userId) {
}

$sct = $calendarObjects->getSchedulingTransparency();

Check failure on line 645 in apps/user_status/lib/Service/StatusService.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

UndefinedInterfaceMethod

apps/user_status/lib/Service/StatusService.php:645:29: UndefinedInterfaceMethod: Method OCP\Calendar\ICalendar::getSchedulingTransparency does not exist (see https://psalm.dev/181)

Check failure

Code scanning / Psalm

UndefinedInterfaceMethod Error

Method OCP\Calendar\ICalendar::getSchedulingTransparency does not exist

if (!empty($sct) && ScheduleCalendarTransp::TRANSPARENT == $sct->getValue()) {
// If a calendar is marked as 'transparent', it means we must
// ignore it for free-busy purposes.
Expand All @@ -635,7 +653,6 @@ private function processCalendarAvailability(string $userId) {
if (!empty($ctz)) {
$vtimezoneObj = Reader::read($ctz);
$calendarTimeZone = $vtimezoneObj->VTIMEZONE->getTimeZone();

Check failure on line 655 in apps/user_status/lib/Service/StatusService.php

View workflow job for this annotation

GitHub Actions / static-code-analysis

UndefinedMethod

apps/user_status/lib/Service/StatusService.php:655:51: UndefinedMethod: Method Sabre\VObject\Property::getTimeZone does not exist (see https://psalm.dev/022)

Check failure

Code scanning / Psalm

UndefinedMethod Error

Method Sabre\VObject\Property::getTimeZone does not exist

Check notice

Code scanning / Psalm

PossiblyNullReference Note

Cannot call method getTimeZone on possibly null value

// Destroy circular references so PHP can garbage collect the object.
$vtimezoneObj->destroy();
}
Expand All @@ -647,15 +664,18 @@ private function processCalendarAvailability(string $userId) {
$dtEnd = new \DateTimeImmutable('+1 hours');
$query->setTimerangeStart($dtStart);
$query->setTimerangeEnd($dtEnd);
$results = $this->calendarManager->searchForPrincipal($query);
if(empty($results)) {
return false;
$calendarEvents = $this->calendarManager->searchForPrincipal($query);
// @todo we can cache that
$vavilability = $this->mapper->getAvailabilityFromPropertiesTable($userId);
if(empty($vavilability) && empty($calendarEvents)) {
// No availability settings and no calendar events, we can stop here
return;
}

$calendarObjects = new VCalendar();
foreach ($results as $objectInfo) {
foreach ($calendarEvents as $calendarEvent) {
$vEvent = new VEvent($calendarObjects, 'VEVENT');
foreach($objectInfo['objects'] as $component) {
foreach($calendarEvent['objects'] as $component) {
foreach ($component as $key => $value) {
$vEvent->add($key, $value[0]);
}
Expand All @@ -672,50 +692,56 @@ private function processCalendarAvailability(string $userId) {
$generator->setBaseObject($vcalendar);
$generator->setTimeZone($calendarTimeZone);

$vavilability = $this->mapper->getAvailabilityFromPropertiesTable($userId);
if (!empty($vavilability)) {
$generator->setVAvailability(
Reader::read(
$vavilability
)
);
}
// Generate the intersection of VAVILABILITY and all VEVENTS in all calendars
$result = $generator->getResult();

// We have the intersection of VAVILABILITY and all VEVENTS in all calendars now
// We only need to handle the first result.
if (!isset($result->VFREEBUSY)) {
return false;
return;
}

/** @var Component $freeBusyComponent */
$freeBusyComponent = $result->VFREEBUSY;
$freeBusyProperties = $freeBusyComponent->select('FREEBUSY');
// If there is no Free-busy property at all, the time-range is empty and available
// If there is no FreeBusy property, the time-range is empty and available
// so there is no need to influence the current status
if (count($freeBusyProperties) === 0) {
return false;
}

// If more than one Free-Busy property was returned, it means that an event
// starts or ends inside this time-range, so it's not available and we return false
if (count($freeBusyProperties) > 1) {
return true;
return;
}

/** @var Property $freeBusyProperty */
$freeBusyProperty = $freeBusyProperties[0];
if (!$freeBusyProperty->offsetExists('FBTYPE')) {
// If there is no FBTYPE, it means it's busy
return true;
// If there is no FBTYPE, it means it's busy from a regular event
// @todo check if it would be better to set the timestamp to the start of the event
$this->setUserStatus($userId, IUserStatus::AWAY, IUserStatus::MESSAGE_CALENDAR_BUSY, false);
return;
}

// If we can't deal with the FBTYPE (custom properties are a possibility)
// we should ignore it and leave the current status
$fbTypeParameter = $freeBusyProperty->offsetGet('FBTYPE');
if (!($fbTypeParameter instanceof Parameter)) {
return false;
return;
}
$fbType = $fbTypeParameter->getValue();
switch ($fbType) {
case 'BUSY':
$this->setUserStatus($userId, IUserStatus::AWAY, IUserStatus::MESSAGE_CALENDAR_BUSY, false);
return;
case 'BUSY-UNAVAILABLE':
$this->setUserStatus($userId, IUserStatus::AWAY, IUserStatus::MESSAGE_AVAILABILITY, false);
return;
case 'BUSY-TENTATIVE':
$this->setUserStatus($userId, IUserStatus::AWAY, IUserStatus::MESSAGE_CALENDAR_BUSY_TENTATIVE, false);
return;
default:
}

$free = (strcasecmp($fbTypeParameter->getValue(), 'FREE') === 0);

return $free;
}
}
12 changes: 12 additions & 0 deletions lib/public/UserStatus/IUserStatus.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,18 @@ interface IUserStatus {
*/
public const MESSAGE_AVAILABILITY = 'availability';

/**
* @var string
* @since 28.0.0
*/
public const MESSAGE_CALENDAR_BUSY = 'busy';

/**
* @var string
* @since 28.0.0
*/
public const MESSAGE_CALENDAR_BUSY_TENTATIVE = 'busy-tentative';

/**
* Get the user this status is connected to
*
Expand Down

0 comments on commit f4cfa4d

Please sign in to comment.