calendarRepository = $calendarRepository; $this->calendarRangeRepository = $calendarRangeRepository; $this->machineHttpClient = $machineHttpClient; $this->mapCalendarToUser = $mapCalendarToUser; $this->logger = $logger; $this->remoteEventConverter = $remoteEventConverter; $this->tokenStorage = $tokenStorage; $this->translator = $translator; $this->urlGenerator = $urlGenerator; $this->userHttpClient = $userHttpClient; $this->security = $security; } public function countEventsForUser(User $user, DateTimeImmutable $startDate, DateTimeImmutable $endDate): int { $userId = $this->mapCalendarToUser->getUserId($user); if (null === $userId) { return 0; } try { $data = $this->userHttpClient->request( 'GET', 'users/' . $userId . '/calendarView', [ 'query' => [ 'startDateTime' => $startDate->setTimezone(RemoteEventConverter::getRemoteTimeZone())->format(RemoteEventConverter::getRemoteDateTimeSimpleFormat()), 'endDateTime' => $endDate->setTimezone(RemoteEventConverter::getRemoteTimeZone())->format(RemoteEventConverter::getRemoteDateTimeSimpleFormat()), '$count' => 'true', '$top' => 0, ], ] )->toArray(); } catch (ClientExceptionInterface $e) { if (403 === $e->getResponse()->getStatusCode()) { return count($this->getScheduleTimesForUser($user, $startDate, $endDate)); } $this->logger->error('Could not get list of event on MSGraph', [ 'error_code' => $e->getResponse()->getStatusCode(), 'error' => $e->getResponse()->getInfo(), ]); return 0; } return $data['@odata.count']; } public function getMakeReadyResponse(string $returnPath): Response { return new RedirectResponse($this->urlGenerator ->generate('chill_calendar_remote_connect_azure', ['returnPath' => $returnPath])); } public function isReady(): bool { $user = $this->security->getUser(); if (!$user instanceof User) { // this is not a user from chill. This is not the role of this class to // restrict access, so we will just say that we do not have to do anything more // here... return true; } if (null === $this->mapCalendarToUser->getUserId($user)) { // this user is not mapped with remote calendar. The user will have to wait for // the next calendar subscription iteration $this->logger->debug('mark user ready for msgraph calendar as he does not have any mapping', [ 'userId' => $user->getId(), ]); return true; } return $this->tokenStorage->hasToken(); } /** * @throws \Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface * @throws \Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface * @throws \Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface * @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface * * @return array|\Chill\CalendarBundle\RemoteCalendar\Model\RemoteEvent[] */ public function listEventsForUser(User $user, DateTimeImmutable $startDate, DateTimeImmutable $endDate, ?int $offset = 0, ?int $limit = 50): array { $userId = $this->mapCalendarToUser->getUserId($user); if (null === $userId) { return []; } try { $bareEvents = $this->userHttpClient->request( 'GET', 'users/' . $userId . '/calendarView', [ 'query' => [ 'startDateTime' => $startDate->setTimezone(RemoteEventConverter::getRemoteTimeZone())->format(RemoteEventConverter::getRemoteDateTimeSimpleFormat()), 'endDateTime' => $endDate->setTimezone(RemoteEventConverter::getRemoteTimeZone())->format(RemoteEventConverter::getRemoteDateTimeSimpleFormat()), '$select' => 'id,subject,start,end,isAllDay', '$top' => $limit, '$skip' => $offset, ], ] )->toArray(); $ids = array_map(static fn ($item) => $item['id'], $bareEvents['value']); $existingIdsInRange = $this->calendarRangeRepository->findRemoteIdsPresent($ids); $existingIdsInCalendar = $this->calendarRepository->findRemoteIdsPresent($ids); return array_values( array_map( fn ($item) => $this->remoteEventConverter->convertToRemote($item), // filter all event to keep only the one not in range array_filter( $bareEvents['value'], static fn ($item) => ((!$existingIdsInRange[$item['id']]) ?? true) && ((!$existingIdsInCalendar[$item['id']]) ?? true) ) ) ); } catch (ClientExceptionInterface $e) { if (403 === $e->getResponse()->getStatusCode()) { return $this->getScheduleTimesForUser($user, $startDate, $endDate); } $this->logger->error('Could not get list of event on MSGraph', [ 'error_code' => $e->getResponse()->getStatusCode(), 'error' => $e->getResponse()->getInfo(), ]); return []; } } public function removeCalendar(string $remoteId, array $remoteAttributes, User $user, ?CalendarRange $associatedCalendarRange = null): void { if ('' === $remoteId) { return; } $this->removeEvent($remoteId, $user); if (null !== $associatedCalendarRange) { $this->syncCalendarRange($associatedCalendarRange); } } public function removeCalendarRange(string $remoteId, array $remoteAttributes, User $user): void { if ('' === $remoteId) { return; } $this->removeEvent($remoteId, $user); } public function syncCalendar(Calendar $calendar, string $action, ?CalendarRange $previousCalendarRange, ?User $previousMainUser, ?array $oldInvites, ?array $newInvites): void { /* * cases to support: * * * a calendar range is created: * * create on remote * * if calendar range is associated: remove the range * * a Calendar change the CalendarRange: * * re-create the previous calendar range; * * remove the current calendar range * * a calendar change the mainUser * * cancel the calendar in the previous mainUser * * recreate the previous calendar range in the previousMainUser, if any * * delete the current calendar range in the current mainUser, if any * * create the calendar in the current mainUser * */ if (!$calendar->hasRemoteId()) { $this->createCalendarOnRemote($calendar); } else { if (null !== $previousMainUser) { // cancel event in previousMainUserCalendar $this->cancelOnRemote( $calendar->getRemoteId(), $this->translator->trans('remote_ms_graph.cancel_event_because_main_user_is_%label%', ['%label%' => $calendar->getMainUser()]), $previousMainUser, 'calendar_' . $calendar->getRemoteId() ); $this->createCalendarOnRemote($calendar); } else { $this->patchCalendarOnRemote($calendar, $newInvites); } } if ($calendar->hasCalendarRange() && $calendar->getCalendarRange()->hasRemoteId()) { $this->removeEvent( $calendar->getCalendarRange()->getRemoteId(), $calendar->getCalendarRange()->getUser() ); $calendar->getCalendarRange() ->addRemoteAttributes([ 'lastModifiedDateTime' => null, 'changeKey' => null, 'previousId' => $calendar->getCalendarRange()->getRemoteId(), ]) ->setRemoteId(''); } if (null !== $previousCalendarRange) { $this->createRemoteCalendarRange($previousCalendarRange); } } public function syncCalendarRange(CalendarRange $calendarRange): void { if ($calendarRange->hasRemoteId()) { $this->updateRemoteCalendarRange($calendarRange); } else { $this->createRemoteCalendarRange($calendarRange); } } public function syncInvite(Invite $invite): void { if ('' === $remoteId = $invite->getCalendar()->getRemoteId()) { return; } if (null === $invite->getUser()) { return; } if (null === $userId = $this->mapCalendarToUser->getUserId($invite->getUser())) { return; } if ($invite->hasRemoteId()) { $remoteIdAttendeeCalendar = $invite->getRemoteId(); } else { $remoteIdAttendeeCalendar = $this->findRemoteIdOnUserCalendar($invite->getCalendar(), $invite->getUser()); $invite->setRemoteId($remoteIdAttendeeCalendar); } switch ($invite->getStatus()) { case Invite::PENDING: return; case Invite::ACCEPTED: $url = "/v1.0/users/{$userId}/calendar/events/{$remoteIdAttendeeCalendar}/accept"; break; case Invite::TENTATIVELY_ACCEPTED: $url = "/v1.0/users/{$userId}/calendar/events/{$remoteIdAttendeeCalendar}/tentativelyAccept"; break; case Invite::DECLINED: $url = "/v1.0/users/{$userId}/calendar/events/{$remoteIdAttendeeCalendar}/decline"; break; default: throw new Exception('not supported'); } try { $this->machineHttpClient->request( 'POST', $url, ['json' => ['sendResponse' => true]] ); } catch (ClientExceptionInterface $e) { $this->logger->warning('could not update calendar range to remote', [ 'exception' => $e->getTraceAsString(), 'content' => $e->getResponse()->getContent(), 'calendarRangeId' => 'invite_' . $invite->getId(), ]); throw $e; } } private function cancelOnRemote(string $remoteId, string $comment, User $user, string $identifier): void { $userId = $this->mapCalendarToUser->getUserId($user); if (null === $userId) { return; } try { $this->machineHttpClient->request( 'POST', "users/{$userId}/calendar/events/{$remoteId}/cancel", [ 'json' => ['Comment' => $comment], ] ); } catch (ClientExceptionInterface $e) { $this->logger->warning('could not update calendar range to remote', [ 'exception' => $e->getTraceAsString(), 'content' => $e->getResponse()->getContent(), 'calendarRangeId' => $identifier, ]); throw $e; } } private function createCalendarOnRemote(Calendar $calendar): void { $eventData = $this->remoteEventConverter->calendarToEvent($calendar); [ 'id' => $id, 'lastModifiedDateTime' => $lastModified, 'changeKey' => $changeKey ] = $this->createOnRemote($eventData, $calendar->getMainUser(), 'calendar_' . $calendar->getId()); if (null === $id) { return; } $calendar ->setRemoteId($id) ->addRemoteAttributes([ 'lastModifiedDateTime' => $lastModified, 'changeKey' => $changeKey, ]); } /** * @param string $identifier an identifier for logging in case of something does not work * * @return array{?id: string, ?lastModifiedDateTime: int, ?changeKey: string} */ private function createOnRemote(array $eventData, User $user, string $identifier): array { $userId = $this->mapCalendarToUser->getUserId($user); if (null === $userId) { $this->logger->warning('user does not have userId nor calendarId', [ 'user_id' => $user->getId(), 'calendar_identifier' => $identifier, ]); return ['id' => null, 'lastModifiedDateTime' => null, 'changeKey' => null]; } try { $event = $this->machineHttpClient->request( 'POST', 'users/' . $userId . '/calendar/events', [ 'json' => $eventData, ] )->toArray(); } catch (ClientExceptionInterface $e) { $this->logger->warning('could not save calendar range to remote', [ 'exception' => $e->getTraceAsString(), 'content' => $e->getResponse()->getContent(), 'calendar_identifier' => $identifier, ]); throw $e; } return [ 'id' => $event['id'], 'lastModifiedDateTime' => $this->remoteEventConverter->getLastModifiedDate($event)->getTimestamp(), 'changeKey' => $event['changeKey'], ]; } private function createRemoteCalendarRange(CalendarRange $calendarRange): void { $userId = $this->mapCalendarToUser->getUserId($calendarRange->getUser()); if (null === $userId) { $this->logger->warning('user does not have userId nor calendarId', [ 'user_id' => $calendarRange->getUser()->getId(), 'calendar_range_id' => $calendarRange->getId(), ]); return; } $eventData = $this->remoteEventConverter->calendarRangeToEvent($calendarRange); [ 'id' => $id, 'lastModifiedDateTime' => $lastModified, 'changeKey' => $changeKey ] = $this->createOnRemote( $eventData, $calendarRange->getUser(), 'calendar_range_' . $calendarRange->getId() ); $calendarRange->setRemoteId($id) ->addRemoteAttributes([ 'lastModifiedDateTime' => $lastModified, 'changeKey' => $changeKey, ]); } /** * the remoteId is not the same across different user calendars. This method allow to find * the correct remoteId in another calendar. * * For achieving this, the iCalUid is used. */ private function findRemoteIdOnUserCalendar(Calendar $calendar, User $user): ?string { // find the icalUid on original user $event = $this->getOnRemote($calendar->getMainUser(), $calendar->getRemoteId()); $userId = $this->mapCalendarToUser->getUserId($user); if ('' === $iCalUid = ($event['iCalUId'] ?? '')) { throw new Exception('no iCalUid for this event'); } try { $events = $this->machineHttpClient->request( 'GET', "/v1.0/users/{$userId}/calendar/events", [ 'query' => [ '$select' => 'id', '$filter' => "iCalUId eq '{$iCalUid}'", ], ] )->toArray(); } catch (ClientExceptionInterface $clientException) { throw $clientException; } if (1 !== count($events['value'])) { throw new Exception('multiple events found with same iCalUid'); } return $events['value'][0]['id']; } private function getOnRemote(User $user, string $remoteId): array { $userId = $this->mapCalendarToUser->getUserId($user); if (null === $userId) { throw new Exception( sprintf( 'no remote calendar for this user: %s, remoteid: %s', $user->getId(), $remoteId ) ); } try { return $this->machineHttpClient->request( 'GET', 'users/' . $userId . '/calendar/events/' . $remoteId )->toArray(); } catch (ClientExceptionInterface $e) { $this->logger->warning('Could not get event from calendar', [ 'remoteId' => $remoteId, ]); throw $e; } } private function getScheduleTimesForUser(User $user, DateTimeImmutable $startDate, DateTimeImmutable $endDate): array { $userId = $this->mapCalendarToUser->getUserId($user); if (array_key_exists($userId, $this->cacheScheduleTimeForUser)) { return $this->cacheScheduleTimeForUser[$userId]; } if (null === $userId) { return []; } if (null === $user->getEmailCanonical() || '' === $user->getEmailCanonical()) { return []; } $body = [ 'schedules' => [$user->getEmailCanonical()], 'startTime' => [ 'dateTime' => ($startDate->setTimezone(RemoteEventConverter::getRemoteTimeZone())->format(RemoteEventConverter::getRemoteDateTimeSimpleFormat())), 'timeZone' => 'UTC', ], 'endTime' => [ 'dateTime' => ($endDate->setTimezone(RemoteEventConverter::getRemoteTimeZone())->format(RemoteEventConverter::getRemoteDateTimeSimpleFormat())), 'timeZone' => 'UTC', ], ]; try { $response = $this->userHttpClient->request('POST', 'users/' . $userId . '/calendar/getSchedule', [ 'json' => $body, ])->toArray(); } catch (ClientExceptionInterface $e) { $this->logger->debug('Could not get schedule on MSGraph', [ 'error_code' => $e->getResponse()->getStatusCode(), 'error' => $e->getResponse()->getInfo(), ]); return []; } $this->cacheScheduleTimeForUser[$userId] = array_map( fn ($item) => $this->remoteEventConverter->convertAvailabilityToRemoteEvent($item), $response['value'][0]['scheduleItems'] ); return $this->cacheScheduleTimeForUser[$userId]; } private function patchCalendarOnRemote(Calendar $calendar, array $newInvites): void { $eventDatas = []; $eventDatas[] = $this->remoteEventConverter->calendarToEvent($calendar); if (0 < count($newInvites)) { // it seems that invitaiton are always send, even if attendee changes are mixed with other datas // $eventDatas[] = $this->remoteEventConverter->calendarToEventAttendeesOnly($calendar); } foreach ($eventDatas as $eventData) { [ 'id' => $id, 'lastModifiedDateTime' => $lastModified, 'changeKey' => $changeKey ] = $this->patchOnRemote( $calendar->getRemoteId(), $eventData, $calendar->getMainUser(), 'calendar_' . $calendar->getId() ); $calendar->addRemoteAttributes([ 'lastModifiedDateTime' => $lastModified, 'changeKey' => $changeKey, ]); } } /** * @param string $identifier an identifier for logging in case of something does not work * * @return array{?id: string, ?lastModifiedDateTime: int, ?changeKey: string} */ private function patchOnRemote(string $remoteId, array $eventData, User $user, string $identifier): array { $userId = $this->mapCalendarToUser->getUserId($user); if (null === $userId) { $this->logger->warning('user does not have userId nor calendarId', [ 'user_id' => $user->getId(), 'calendar_identifier' => $identifier, ]); return ['id' => null, 'lastModifiedDateTime' => null, 'changeKey' => null]; } try { $event = $this->machineHttpClient->request( 'PATCH', 'users/' . $userId . '/calendar/events/' . $remoteId, [ 'json' => $eventData, ] )->toArray(); } catch (ClientExceptionInterface $e) { $this->logger->warning('could not update calendar range to remote', [ 'exception' => $e->getTraceAsString(), 'calendarRangeId' => $identifier, ]); throw $e; } return [ 'id' => $event['id'], 'lastModifiedDateTime' => $this->remoteEventConverter->getLastModifiedDate($event)->getTimestamp(), 'changeKey' => $event['changeKey'], ]; } private function removeEvent($remoteId, User $user): void { $userId = $this->mapCalendarToUser->getUserId($user); try { $this->machineHttpClient->request( 'DELETE', 'users/' . $userId . '/calendar/events/' . $remoteId ); } catch (ClientExceptionInterface $e) { $this->logger->warning('could not remove event from calendar', [ 'event_remote_id' => $remoteId, 'user_id' => $user->getId(), ]); } } private function updateRemoteCalendarRange(CalendarRange $calendarRange): void { $userId = $this->mapCalendarToUser->getUserId($calendarRange->getUser()); $calendarId = $this->mapCalendarToUser->getCalendarId($calendarRange->getUser()); if (null === $userId || null === $calendarId) { $this->logger->warning('user does not have userId nor calendarId', [ 'user_id' => $calendarRange->getUser()->getId(), 'calendar_range_id' => $calendarRange->getId(), ]); return; } try { $event = $this->machineHttpClient->request( 'GET', 'users/' . $userId . '/calendar/events/' . $calendarRange->getRemoteId() )->toArray(); } catch (ClientExceptionInterface $e) { $this->logger->warning('Could not get event from calendar', [ 'calendar_range_id' => $calendarRange->getId(), 'calendar_range_remote_id' => $calendarRange->getRemoteId(), ]); throw $e; } if ($this->remoteEventConverter->getLastModifiedDate($event)->getTimestamp() > $calendarRange->getUpdatedAt()->getTimestamp()) { $this->logger->info('Skip updating as the lastModified date seems more fresh than the database one', [ 'calendar_range_id' => $calendarRange->getId(), 'calendar_range_remote_id' => $calendarRange->getRemoteId(), 'db_last_updated' => $calendarRange->getUpdatedAt()->getTimestamp(), 'remote_last_updated' => $this->remoteEventConverter->getLastModifiedDate($event)->getTimestamp(), ]); return; } $eventData = $this->remoteEventConverter->calendarRangeToEvent($calendarRange); try { $event = $this->machineHttpClient->request( 'PATCH', 'users/' . $userId . '/calendar/events/' . $calendarRange->getRemoteId(), [ 'json' => $eventData, ] )->toArray(); } catch (ClientExceptionInterface $e) { $this->logger->warning('could not update calendar range to remote', [ 'exception' => $e->getTraceAsString(), 'calendarRangeId' => $calendarRange->getId(), ]); throw $e; } $calendarRange ->addRemoteAttributes([ 'lastModifiedDateTime' => $this->remoteEventConverter->getLastModifiedDate($event)->getTimestamp(), 'changeKey' => $event['changeKey'], ]); } }