StatusService.php 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * @copyright 2023 Anna Larch <anna.larch@gmx.net>
  5. *
  6. * @author Anna Larch <anna.larch@gmx.net>
  7. *
  8. * @license GNU AGPL version 3 or any later version
  9. *
  10. * This program is free software: you can redistribute it and/or modify
  11. * it under the terms of the GNU Affero General Public License as
  12. * published by the Free Software Foundation, either version 3 of the
  13. * License, or (at your option) any later version.
  14. *
  15. * This program is distributed in the hope that it will be useful,
  16. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  18. * GNU Affero General Public License for more details.
  19. *
  20. * You should have received a copy of the GNU Affero General Public License
  21. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  22. *
  23. */
  24. namespace OCA\DAV\CalDAV\Status;
  25. use DateTimeImmutable;
  26. use OC\Calendar\CalendarQuery;
  27. use OCA\DAV\CalDAV\CalendarImpl;
  28. use OCA\UserStatus\Service\StatusService as UserStatusService;
  29. use OCP\AppFramework\Db\DoesNotExistException;
  30. use OCP\AppFramework\Utility\ITimeFactory;
  31. use OCP\Calendar\IManager;
  32. use OCP\ICache;
  33. use OCP\ICacheFactory;
  34. use OCP\IUser as User;
  35. use OCP\IUserManager;
  36. use OCP\User\IAvailabilityCoordinator;
  37. use OCP\UserStatus\IUserStatus;
  38. use Psr\Log\LoggerInterface;
  39. use Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp;
  40. class StatusService {
  41. private ICache $cache;
  42. public function __construct(private ITimeFactory $timeFactory,
  43. private IManager $calendarManager,
  44. private IUserManager $userManager,
  45. private UserStatusService $userStatusService,
  46. private IAvailabilityCoordinator $availabilityCoordinator,
  47. private ICacheFactory $cacheFactory,
  48. private LoggerInterface $logger) {
  49. $this->cache = $cacheFactory->createLocal('CalendarStatusService');
  50. }
  51. public function processCalendarStatus(string $userId): void {
  52. $user = $this->userManager->get($userId);
  53. if($user === null) {
  54. return;
  55. }
  56. $availability = $this->availabilityCoordinator->getCurrentOutOfOfficeData($user);
  57. if($availability !== null && $this->availabilityCoordinator->isInEffect($availability)) {
  58. $this->logger->debug('An Absence is in effect, skipping calendar status check', ['user' => $userId]);
  59. return;
  60. }
  61. $calendarEvents = $this->cache->get($userId);
  62. if($calendarEvents === null) {
  63. $calendarEvents = $this->getCalendarEvents($user);
  64. $this->cache->set($userId, $calendarEvents, 300);
  65. }
  66. if(empty($calendarEvents)) {
  67. $this->userStatusService->revertUserStatus($userId, IUserStatus::MESSAGE_CALENDAR_BUSY);
  68. $this->logger->debug('No calendar events found for status check', ['user' => $userId]);
  69. return;
  70. }
  71. try {
  72. $currentStatus = $this->userStatusService->findByUserId($userId);
  73. // Was the status set by anything other than the calendar automation?
  74. $userStatusTimestamp = $currentStatus->getIsUserDefined() && $currentStatus->getMessageId() !== IUserStatus::MESSAGE_CALENDAR_BUSY ? $currentStatus->getStatusTimestamp() : null;
  75. } catch (DoesNotExistException) {
  76. $userStatusTimestamp = null;
  77. $currentStatus = null;
  78. }
  79. if($currentStatus !== null && $currentStatus->getMessageId() === IUserStatus::MESSAGE_CALL
  80. || $currentStatus !== null && $currentStatus->getStatus() === IUserStatus::DND
  81. || $currentStatus !== null && $currentStatus->getStatus() === IUserStatus::INVISIBLE) {
  82. // We don't overwrite the call status, DND status or Invisible status
  83. $this->logger->debug('Higher priority status detected, skipping calendar status change', ['user' => $userId]);
  84. return;
  85. }
  86. // Filter events to see if we have any that apply to the calendar status
  87. $applicableEvents = array_filter($calendarEvents, static function (array $calendarEvent) use ($userStatusTimestamp): bool {
  88. if (empty($calendarEvent['objects'])) {
  89. return false;
  90. }
  91. $component = $calendarEvent['objects'][0];
  92. if (isset($component['X-NEXTCLOUD-OUT-OF-OFFICE'])) {
  93. return false;
  94. }
  95. if (isset($component['DTSTART']) && $userStatusTimestamp !== null) {
  96. /** @var DateTimeImmutable $dateTime */
  97. $dateTime = $component['DTSTART'][0];
  98. if($dateTime instanceof DateTimeImmutable && $userStatusTimestamp > $dateTime->getTimestamp()) {
  99. return false;
  100. }
  101. }
  102. // Ignore events that are transparent
  103. if (isset($component['TRANSP']) && strcasecmp($component['TRANSP'][0], 'TRANSPARENT') === 0) {
  104. return false;
  105. }
  106. return true;
  107. });
  108. if(empty($applicableEvents)) {
  109. $this->userStatusService->revertUserStatus($userId, IUserStatus::MESSAGE_CALENDAR_BUSY);
  110. $this->logger->debug('No status relevant events found, skipping calendar status change', ['user' => $userId]);
  111. return;
  112. }
  113. // Only update the status if it's neccesary otherwise we mess up the timestamp
  114. if($currentStatus === null || $currentStatus->getMessageId() !== IUserStatus::MESSAGE_CALENDAR_BUSY) {
  115. // One event that fulfills all status conditions is enough
  116. // 1. Not an OOO event
  117. // 2. Current user status (that is not a calendar status) was not set after the start of this event
  118. // 3. Event is not set to be transparent
  119. $count = count($applicableEvents);
  120. $this->logger->debug("Found $count applicable event(s), changing user status", ['user' => $userId]);
  121. $this->userStatusService->setUserStatus(
  122. $userId,
  123. IUserStatus::AWAY,
  124. IUserStatus::MESSAGE_CALENDAR_BUSY,
  125. true
  126. );
  127. }
  128. }
  129. private function getCalendarEvents(User $user): array {
  130. $calendars = $this->calendarManager->getCalendarsForPrincipal('principals/users/' . $user->getUID());
  131. if(empty($calendars)) {
  132. return [];
  133. }
  134. $query = $this->calendarManager->newQuery('principals/users/' . $user->getUID());
  135. foreach ($calendars as $calendarObject) {
  136. // We can only work with a calendar if it exposes its scheduling information
  137. if (!$calendarObject instanceof CalendarImpl) {
  138. continue;
  139. }
  140. $sct = $calendarObject->getSchedulingTransparency();
  141. if ($sct !== null && ScheduleCalendarTransp::TRANSPARENT == strtolower($sct->getValue())) {
  142. // If a calendar is marked as 'transparent', it means we must
  143. // ignore it for free-busy purposes.
  144. continue;
  145. }
  146. $query->addSearchCalendar($calendarObject->getUri());
  147. }
  148. $dtStart = DateTimeImmutable::createFromMutable($this->timeFactory->getDateTime());
  149. $dtEnd = DateTimeImmutable::createFromMutable($this->timeFactory->getDateTime('+5 minutes'));
  150. // Only query the calendars when there's any to search
  151. if($query instanceof CalendarQuery && !empty($query->getCalendarUris())) {
  152. // Query the next hour
  153. $query->setTimerangeStart($dtStart);
  154. $query->setTimerangeEnd($dtEnd);
  155. return $this->calendarManager->searchForPrincipal($query);
  156. }
  157. return [];
  158. }
  159. }