Skip to content

Commit 6c3411e

Browse files
committed
fix(userstatus): set user status to 'In a meeting' if calendar is busy
Signed-off-by: Anna Larch <anna@nextcloud.com>
1 parent d8381ac commit 6c3411e

8 files changed

Lines changed: 431 additions & 855 deletions

File tree

apps/dav/lib/CalDAV/Status/Status.php

Lines changed: 0 additions & 72 deletions
This file was deleted.

apps/dav/lib/CalDAV/Status/StatusService.php

Lines changed: 93 additions & 116 deletions
Original file line numberDiff line numberDiff line change
@@ -25,83 +25,123 @@
2525
*/
2626
namespace OCA\DAV\CalDAV\Status;
2727

28+
use DateTimeImmutable;
2829
use OC\Calendar\CalendarQuery;
2930
use OCA\DAV\CalDAV\CalendarImpl;
30-
use OCA\DAV\CalDAV\FreeBusy\FreeBusyGenerator;
31-
use OCA\DAV\CalDAV\InvitationResponse\InvitationResponseServer;
32-
use OCA\DAV\CalDAV\Schedule\Plugin as SchedulePlugin;
31+
use OCA\UserStatus\Service\StatusService as UserStatusService;
32+
use OCP\AppFramework\Db\DoesNotExistException;
3333
use OCP\AppFramework\Utility\ITimeFactory;
3434
use OCP\Calendar\IManager;
35-
use OCP\IL10N;
35+
use OCP\ICache;
36+
use OCP\ICacheFactory;
3637
use OCP\IUser as User;
38+
use OCP\IUserManager;
39+
use OCP\User\IAvailabilityCoordinator;
3740
use OCP\UserStatus\IUserStatus;
41+
use Psr\Log\LoggerInterface;
3842
use Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp;
39-
use Sabre\DAV\Exception\NotAuthenticated;
40-
use Sabre\DAVACL\Exception\NeedPrivileges;
41-
use Sabre\DAVACL\Plugin as AclPlugin;
42-
use Sabre\VObject\Component;
43-
use Sabre\VObject\Component\VEvent;
44-
use Sabre\VObject\Parameter;
45-
use Sabre\VObject\Property;
4643

4744
class StatusService {
45+
private ICache $cache;
4846
public function __construct(private ITimeFactory $timeFactory,
4947
private IManager $calendarManager,
50-
private InvitationResponseServer $server,
51-
private IL10N $l10n,
52-
private FreeBusyGenerator $generator) {
48+
private IUserManager $userManager,
49+
private UserStatusService $userStatusService,
50+
private IAvailabilityCoordinator $availabilityCoordinator,
51+
private ICacheFactory $cacheFactory,
52+
private LoggerInterface $logger) {
53+
$this->cache = $cacheFactory->createLocal('CalendarStatusService');
5354
}
5455

55-
public function processCalendarAvailability(User $user): ?Status {
56-
$userId = $user->getUID();
57-
$email = $user->getEMailAddress();
58-
if($email === null) {
59-
return null;
56+
public function processCalendarStatus(string $userId): void {
57+
$user = $this->userManager->get($userId);
58+
if($user === null) {
59+
return;
6060
}
6161

62-
$server = $this->server->getServer();
62+
$availability = $this->availabilityCoordinator->getCurrentOutOfOfficeData($user);
63+
if($availability !== null && $this->availabilityCoordinator->isInEffect($availability)) {
64+
$this->logger->debug('An Absence is in effect, skipping calendar status check', ['user' => $userId]);
65+
return;
66+
}
6367

64-
/** @var SchedulePlugin $schedulingPlugin */
65-
$schedulingPlugin = $server->getPlugin('caldav-schedule');
66-
$caldavNS = '{'.$schedulingPlugin::NS_CALDAV.'}';
68+
$calendarEvents = $this->cache->get($userId);
69+
if($calendarEvents === null) {
70+
$calendarEvents = $this->getCalendarEvents($user);
71+
$this->cache->set($userId, $calendarEvents, 300);
72+
}
6773

68-
/** @var AclPlugin $aclPlugin */
69-
$aclPlugin = $server->getPlugin('acl');
70-
if ('mailto:' === substr($email, 0, 7)) {
71-
$email = substr($email, 7);
74+
if(empty($calendarEvents)) {
75+
$this->userStatusService->revertUserStatus($userId, IUserStatus::MESSAGE_CALENDAR_BUSY);
76+
$this->logger->debug('No calendar events found for status check', ['user' => $userId]);
77+
return;
7278
}
7379

74-
$result = $aclPlugin->principalSearch(
75-
['{http://sabredav.org/ns}email-address' => $email],
76-
[
77-
'{DAV:}principal-URL',
78-
$caldavNS.'calendar-home-set',
79-
$caldavNS.'schedule-inbox-URL',
80-
'{http://sabredav.org/ns}email-address',
81-
]
82-
);
80+
$userStatusTimestamp = null;
81+
$currentStatus = null;
82+
try {
83+
$currentStatus = $this->userStatusService->findByUserId($userId);
84+
$userStatusTimestamp = $currentStatus->getIsUserDefined() ? $currentStatus->getStatusTimestamp() : null;
85+
} catch (DoesNotExistException) {
86+
}
8387

84-
if (!count($result) || !isset($result[0][200][$caldavNS.'schedule-inbox-URL'])) {
85-
return null;
88+
if($currentStatus !== null && $currentStatus->getMessageId() === IUserStatus::MESSAGE_CALL
89+
|| $currentStatus !== null && $currentStatus->getStatus() === IUserStatus::DND
90+
|| $currentStatus !== null && $currentStatus->getStatus() === IUserStatus::INVISIBLE) {
91+
// We don't overwrite the call status, DND status or Invisible status
92+
$this->logger->debug('Higher priority status detected, skipping calendar status change', ['user' => $userId]);
93+
return;
8694
}
8795

88-
$inboxUrl = $result[0][200][$caldavNS.'schedule-inbox-URL']->getHref();
96+
// Filter events to see if we have any that apply to the calendar status
97+
$applicableEvents = array_filter($calendarEvents, function (array $calendarEvent) use ($userStatusTimestamp) {
98+
$component = $calendarEvent['objects'][0];
99+
if(isset($component['X-NEXTCLOUD-OUT-OF-OFFICE'])) {
100+
return false;
101+
}
102+
if(isset($component['DTSTART']) && $userStatusTimestamp !== null) {
103+
/** @var DateTimeImmutable $dateTime */
104+
$dateTime = $component['DTSTART'][0];
105+
$timestamp = $dateTime->getTimestamp();
106+
if($userStatusTimestamp > $timestamp) {
107+
return false;
108+
}
109+
}
110+
// Ignore events that are transparent
111+
if(isset($component['TRANSP']) && strcasecmp($component['TRANSP'][0], 'TRANSPARENT') === 0) {
112+
return false;
113+
}
114+
return true;
115+
});
89116

90-
// Do we have permission?
91-
try {
92-
$aclPlugin->checkPrivileges($inboxUrl, $caldavNS.'schedule-query-freebusy');
93-
} catch (NeedPrivileges | NotAuthenticated $exception) {
94-
return null;
117+
if(empty($applicableEvents)) {
118+
$this->userStatusService->revertUserStatus($userId, IUserStatus::MESSAGE_CALENDAR_BUSY);
119+
$this->logger->debug('No status relevant events found, skipping calendar status change', ['user' => $userId]);
120+
return;
95121
}
96122

97-
$now = $this->timeFactory->now();
98-
$calendarTimeZone = $now->getTimezone();
99-
$calendars = $this->calendarManager->getCalendarsForPrincipal('principals/users/' . $userId);
123+
// One event that fulfills all status conditions is enough
124+
// 1. Not an OOO event
125+
// 2. Current user status was not set after the start of this event
126+
// 3. Event is not set to be transparent
127+
$count = count($applicableEvents);
128+
$this->logger->debug("Found $count applicable event(s), changing user status", ['user' => $userId]);
129+
$this->userStatusService->setUserStatus(
130+
$userId,
131+
IUserStatus::AWAY,
132+
IUserStatus::MESSAGE_CALENDAR_BUSY,
133+
true
134+
);
135+
136+
}
137+
138+
private function getCalendarEvents(User $user): array {
139+
$calendars = $this->calendarManager->getCalendarsForPrincipal('principals/users/' . $user->getUID());
100140
if(empty($calendars)) {
101-
return null;
141+
return [];
102142
}
103143

104-
$query = $this->calendarManager->newQuery('principals/users/' . $userId);
144+
$query = $this->calendarManager->newQuery('principals/users/' . $user->getUID());
105145
foreach ($calendars as $calendarObject) {
106146
// We can only work with a calendar if it exposes its scheduling information
107147
if (!$calendarObject instanceof CalendarImpl) {
@@ -114,83 +154,20 @@ public function processCalendarAvailability(User $user): ?Status {
114154
// ignore it for free-busy purposes.
115155
continue;
116156
}
117-
118-
/** @var Component\VTimeZone|null $ctz */
119-
$ctz = $calendarObject->getSchedulingTimezone();
120-
if ($ctz !== null) {
121-
$calendarTimeZone = $ctz->getTimeZone();
122-
}
123157
$query->addSearchCalendar($calendarObject->getUri());
124158
}
125159

126-
$calendarEvents = [];
127-
$dtStart = $now;
128-
$dtEnd = \DateTimeImmutable::createFromMutable($this->timeFactory->getDateTime('+10 minutes'));
160+
$dtStart = DateTimeImmutable::createFromMutable($this->timeFactory->getDateTime());
161+
$dtEnd = DateTimeImmutable::createFromMutable($this->timeFactory->getDateTime('+5 minutes'));
129162

130163
// Only query the calendars when there's any to search
131164
if($query instanceof CalendarQuery && !empty($query->getCalendarUris())) {
132165
// Query the next hour
133166
$query->setTimerangeStart($dtStart);
134167
$query->setTimerangeEnd($dtEnd);
135-
$calendarEvents = $this->calendarManager->searchForPrincipal($query);
168+
return $this->calendarManager->searchForPrincipal($query);
136169
}
137170

138-
// @todo we can cache that
139-
if(empty($calendarEvents)) {
140-
return null;
141-
}
142-
143-
$calendar = $this->generator->getVCalendar();
144-
foreach ($calendarEvents as $calendarEvent) {
145-
$vEvent = new VEvent($calendar, 'VEVENT');
146-
foreach($calendarEvent['objects'] as $component) {
147-
foreach ($component as $key => $value) {
148-
$vEvent->add($key, $value[0]);
149-
}
150-
}
151-
$calendar->add($vEvent);
152-
}
153-
154-
$calendar->METHOD = 'REQUEST';
155-
156-
$this->generator->setObjects($calendar);
157-
$this->generator->setTimeRange($dtStart, $dtEnd);
158-
$this->generator->setTimeZone($calendarTimeZone);
159-
$result = $this->generator->getResult();
160-
161-
if (!isset($result->VFREEBUSY)) {
162-
return null;
163-
}
164-
165-
/** @var Component $freeBusyComponent */
166-
$freeBusyComponent = $result->VFREEBUSY;
167-
$freeBusyProperties = $freeBusyComponent->select('FREEBUSY');
168-
// If there is no FreeBusy property, the time-range is empty and available
169-
if (count($freeBusyProperties) === 0) {
170-
return null;
171-
}
172-
173-
/** @var Property $freeBusyProperty */
174-
$freeBusyProperty = $freeBusyProperties[0];
175-
if (!$freeBusyProperty->offsetExists('FBTYPE')) {
176-
// If there is no FBTYPE, it means it's busy from a regular event
177-
return new Status(IUserStatus::BUSY, IUserStatus::MESSAGE_CALENDAR_BUSY);
178-
}
179-
180-
// If we can't deal with the FBTYPE (custom properties are a possibility)
181-
// we should ignore it and leave the current status
182-
$fbTypeParameter = $freeBusyProperty->offsetGet('FBTYPE');
183-
if (!($fbTypeParameter instanceof Parameter)) {
184-
return null;
185-
}
186-
$fbType = $fbTypeParameter->getValue();
187-
switch ($fbType) {
188-
// Ignore BUSY-UNAVAILABLE, that's for the automation
189-
case 'BUSY':
190-
case 'BUSY-TENTATIVE':
191-
return new Status(IUserStatus::BUSY, IUserStatus::MESSAGE_CALENDAR_BUSY, $this->l10n->t('In a meeting'));
192-
default:
193-
return null;
194-
}
171+
return [];
195172
}
196173
}

0 commit comments

Comments
 (0)