You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

StatusService.php 7.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. <?php
  2. /*
  3. * *
  4. * * Dav App
  5. * *
  6. * * @copyright 2023 Anna Larch <anna.larch@gmx.net>
  7. * *
  8. * * @author Anna Larch <anna.larch@gmx.net>
  9. * *
  10. * * This library is free software; you can redistribute it and/or
  11. * * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
  12. * * License as published by the Free Software Foundation; either
  13. * * version 3 of the License, or any later version.
  14. * *
  15. * * This library 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
  21. * * License along with this library. If not, see <http://www.gnu.org/licenses/>.
  22. * *
  23. *
  24. */
  25. declare(strict_types=1);
  26. /**
  27. * @copyright 2023 Anna Larch <anna.larch@gmx.net>
  28. *
  29. * @author Anna Larch <anna.larch@gmx.net>
  30. *
  31. * @license GNU AGPL version 3 or any later version
  32. *
  33. * This program is free software: you can redistribute it and/or modify
  34. * it under the terms of the GNU Affero General Public License as
  35. * published by the Free Software Foundation, either version 3 of the
  36. * License, or (at your option) any later version.
  37. *
  38. * This program is distributed in the hope that it will be useful,
  39. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  40. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  41. * GNU Affero General Public License for more details.
  42. *
  43. * You should have received a copy of the GNU Affero General Public License
  44. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  45. *
  46. */
  47. namespace OCA\DAV\CalDAV\Status;
  48. use OC\Calendar\CalendarQuery;
  49. use OCA\DAV\CalDAV\CalendarImpl;
  50. use OCA\DAV\CalDAV\FreeBusy\FreeBusyGenerator;
  51. use OCA\DAV\CalDAV\InvitationResponse\InvitationResponseServer;
  52. use OCA\DAV\CalDAV\Schedule\Plugin as SchedulePlugin;
  53. use OCP\AppFramework\Utility\ITimeFactory;
  54. use OCP\Calendar\IManager;
  55. use OCP\IL10N;
  56. use OCP\IUser as User;
  57. use OCP\UserStatus\IUserStatus;
  58. use Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp;
  59. use Sabre\DAV\Exception\NotAuthenticated;
  60. use Sabre\DAVACL\Exception\NeedPrivileges;
  61. use Sabre\DAVACL\Plugin as AclPlugin;
  62. use Sabre\VObject\Component;
  63. use Sabre\VObject\Component\VEvent;
  64. use Sabre\VObject\Parameter;
  65. use Sabre\VObject\Property;
  66. use Sabre\VObject\Reader;
  67. class StatusService {
  68. public function __construct(private ITimeFactory $timeFactory,
  69. private IManager $calendarManager,
  70. private InvitationResponseServer $server,
  71. private IL10N $l10n,
  72. private FreeBusyGenerator $generator) {
  73. }
  74. public function processCalendarAvailability(User $user, ?string $availability): ?Status {
  75. $userId = $user->getUID();
  76. $email = $user->getEMailAddress();
  77. if($email === null) {
  78. return null;
  79. }
  80. $server = $this->server->getServer();
  81. /** @var SchedulePlugin $schedulingPlugin */
  82. $schedulingPlugin = $server->getPlugin('caldav-schedule');
  83. $caldavNS = '{'.$schedulingPlugin::NS_CALDAV.'}';
  84. /** @var AclPlugin $aclPlugin */
  85. $aclPlugin = $server->getPlugin('acl');
  86. if ('mailto:' === substr($email, 0, 7)) {
  87. $email = substr($email, 7);
  88. }
  89. $result = $aclPlugin->principalSearch(
  90. ['{http://sabredav.org/ns}email-address' => $email],
  91. [
  92. '{DAV:}principal-URL',
  93. $caldavNS.'calendar-home-set',
  94. $caldavNS.'schedule-inbox-URL',
  95. '{http://sabredav.org/ns}email-address',
  96. ]
  97. );
  98. if (!count($result) || !isset($result[0][200][$caldavNS.'schedule-inbox-URL'])) {
  99. return null;
  100. }
  101. $inboxUrl = $result[0][200][$caldavNS.'schedule-inbox-URL']->getHref();
  102. // Do we have permission?
  103. try {
  104. $aclPlugin->checkPrivileges($inboxUrl, $caldavNS.'schedule-query-freebusy');
  105. } catch (NeedPrivileges | NotAuthenticated $exception) {
  106. return null;
  107. }
  108. $now = $this->timeFactory->now();
  109. $calendarTimeZone = $now->getTimezone();
  110. $calendars = $this->calendarManager->getCalendarsForPrincipal('principals/users/' . $userId);
  111. if(empty($calendars)) {
  112. return null;
  113. }
  114. $query = $this->calendarManager->newQuery('principals/users/' . $userId);
  115. foreach ($calendars as $calendarObject) {
  116. // We can only work with a calendar if it exposes its scheduling information
  117. if (!$calendarObject instanceof CalendarImpl) {
  118. continue;
  119. }
  120. $sct = $calendarObject->getSchedulingTransparency();
  121. if ($sct !== null && ScheduleCalendarTransp::TRANSPARENT == strtolower($sct->getValue())) {
  122. // If a calendar is marked as 'transparent', it means we must
  123. // ignore it for free-busy purposes.
  124. continue;
  125. }
  126. /** @var Component\VTimeZone|null $ctz */
  127. $ctz = $calendarObject->getSchedulingTimezone();
  128. if ($ctz !== null) {
  129. $calendarTimeZone = $ctz->getTimeZone();
  130. }
  131. $query->addSearchCalendar($calendarObject->getUri());
  132. }
  133. $calendarEvents = [];
  134. $dtStart = $now;
  135. $dtEnd = \DateTimeImmutable::createFromMutable($this->timeFactory->getDateTime('+10 minutes'));
  136. // Only query the calendars when there's any to search
  137. if($query instanceof CalendarQuery && !empty($query->getCalendarUris())) {
  138. // Query the next hour
  139. $query->setTimerangeStart($dtStart);
  140. $query->setTimerangeEnd($dtEnd);
  141. $calendarEvents = $this->calendarManager->searchForPrincipal($query);
  142. }
  143. // @todo we can cache that
  144. if(empty($availability) && empty($calendarEvents)) {
  145. // No availability settings and no calendar events, we can stop here
  146. return null;
  147. }
  148. $calendar = $this->generator->getVCalendar();
  149. foreach ($calendarEvents as $calendarEvent) {
  150. $vEvent = new VEvent($calendar, 'VEVENT');
  151. foreach($calendarEvent['objects'] as $component) {
  152. foreach ($component as $key => $value) {
  153. $vEvent->add($key, $value[0]);
  154. }
  155. }
  156. $calendar->add($vEvent);
  157. }
  158. $calendar->METHOD = 'REQUEST';
  159. $this->generator->setObjects($calendar);
  160. $this->generator->setTimeRange($dtStart, $dtEnd);
  161. $this->generator->setTimeZone($calendarTimeZone);
  162. if (!empty($availability)) {
  163. $this->generator->setVAvailability(
  164. Reader::read(
  165. $availability
  166. )
  167. );
  168. }
  169. // Generate the intersection of VAVILABILITY and all VEVENTS in all calendars
  170. $result = $this->generator->getResult();
  171. if (!isset($result->VFREEBUSY)) {
  172. return null;
  173. }
  174. /** @var Component $freeBusyComponent */
  175. $freeBusyComponent = $result->VFREEBUSY;
  176. $freeBusyProperties = $freeBusyComponent->select('FREEBUSY');
  177. // If there is no FreeBusy property, the time-range is empty and available
  178. // so set the status to online as otherwise we will never recover from a BUSY status
  179. if (count($freeBusyProperties) === 0) {
  180. return new Status(IUserStatus::ONLINE);
  181. }
  182. /** @var Property $freeBusyProperty */
  183. $freeBusyProperty = $freeBusyProperties[0];
  184. if (!$freeBusyProperty->offsetExists('FBTYPE')) {
  185. // If there is no FBTYPE, it means it's busy from a regular event
  186. return new Status(IUserStatus::BUSY, IUserStatus::MESSAGE_CALENDAR_BUSY);
  187. }
  188. // If we can't deal with the FBTYPE (custom properties are a possibility)
  189. // we should ignore it and leave the current status
  190. $fbTypeParameter = $freeBusyProperty->offsetGet('FBTYPE');
  191. if (!($fbTypeParameter instanceof Parameter)) {
  192. return null;
  193. }
  194. $fbType = $fbTypeParameter->getValue();
  195. switch ($fbType) {
  196. case 'BUSY':
  197. return new Status(IUserStatus::BUSY, IUserStatus::MESSAGE_CALENDAR_BUSY, $this->l10n->t('In a meeting'));
  198. case 'BUSY-UNAVAILABLE':
  199. return new Status(IUserStatus::AWAY, IUserStatus::MESSAGE_AVAILABILITY);
  200. case 'BUSY-TENTATIVE':
  201. return new Status(IUserStatus::AWAY, IUserStatus::MESSAGE_CALENDAR_BUSY_TENTATIVE);
  202. default:
  203. return null;
  204. }
  205. }
  206. }