From 5b42b85b503c23eda24d5c774d05f4524fc70a72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 6 Jul 2023 16:58:24 +0200 Subject: [PATCH 1/7] Read absence from MS graph api --- .../Exception/UserAbsenceSyncException.php | 20 ++ .../Connector/MSGraph/MSUserAbsenceReader.php | 68 +++++++ .../MSGraph/MSUserAbsenceReaderTest.php | 176 ++++++++++++++++++ 3 files changed, 264 insertions(+) create mode 100644 src/Bundle/ChillCalendarBundle/Exception/UserAbsenceSyncException.php create mode 100644 src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReader.php create mode 100644 src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReaderTest.php diff --git a/src/Bundle/ChillCalendarBundle/Exception/UserAbsenceSyncException.php b/src/Bundle/ChillCalendarBundle/Exception/UserAbsenceSyncException.php new file mode 100644 index 000000000..b46c93976 --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Exception/UserAbsenceSyncException.php @@ -0,0 +1,20 @@ +mapCalendarToUser->getUserId($user); + + if (null === $id) { + return null; + } + + try { + $automaticRepliesSettings = $this->machineHttpClient + ->request('GET', '/users/' . $id . '/mailboxSettings/automaticRepliesSetting') + ->toArray(true); + } catch (ClientExceptionInterface|DecodingExceptionInterface|RedirectionExceptionInterface|TransportExceptionInterface $e) { + throw new UserAbsenceSyncException("Error receiving response for mailboxSettings", 0, $e); + } catch (ServerExceptionInterface $e) { + throw new UserAbsenceSyncException("Server error receiving response for mailboxSettings", 0, $e); + } + + if (!array_key_exists("status", $automaticRepliesSettings)) { + throw new \LogicException("no key \"status\" on automatic replies settings: " . json_encode($automaticRepliesSettings)); + } + + return match ($automaticRepliesSettings['status']) { + 'disabled' => false, + 'alwaysEnabled' => true, + 'scheduled' => + RemoteEventConverter::convertStringDateWithoutTimezone($automaticRepliesSettings['scheduledStartDateTime']['dateTime']) < $this->clock->now() + && RemoteEventConverter::convertStringDateWithoutTimezone($automaticRepliesSettings['scheduledEndDateTime']['dateTime']) > $this->clock->now(), + default => throw new UserAbsenceSyncException("this status is not documented by Microsoft") + }; + } + +} diff --git a/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReaderTest.php b/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReaderTest.php new file mode 100644 index 000000000..089477fda --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReaderTest.php @@ -0,0 +1,176 @@ +prophesize(MapCalendarToUser::class); + $mapUser->getUserId($user)->willReturn('1234'); + $clock = new MockClock(new \DateTimeImmutable('2023-07-07T12:00:00')); + + $absenceReader = new MSUserAbsenceReader($client, $mapUser->reveal(), $clock); + + self::assertEquals($expected, $absenceReader->isUserAbsent($user), $message); + } + + public function testIsUserAbsentWithoutRemoteId(): void + { + $user = new User(); + $client = new MockHttpClient(); + + $mapUser = $this->prophesize(MapCalendarToUser::class); + $mapUser->getUserId($user)->willReturn(null); + $clock = new MockClock(new \DateTimeImmutable('2023-07-07T12:00:00')); + + $absenceReader = new MSUserAbsenceReader($client, $mapUser->reveal(), $clock); + + self::assertNull($absenceReader->isUserAbsent($user), "when no user found, absence should be null"); + } + + public function provideDataTestUserAbsence(): iterable + { + // contains data that was retrieved from microsoft graph api on 2023-07-06 + + yield [ + <<<'JSON' + { + "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('4feb0ae3-7ffb-48dd-891e-c86b2cdeefd4')/mailboxSettings/automaticRepliesSetting", + "status": "disabled", + "externalAudience": "none", + "internalReplyMessage": "Je suis en congé.", + "externalReplyMessage": "", + "scheduledStartDateTime": { + "dateTime": "2023-07-06T12:00:00.0000000", + "timeZone": "UTC" + }, + "scheduledEndDateTime": { + "dateTime": "2023-07-07T12:00:00.0000000", + "timeZone": "UTC" + } + } + JSON, + false, + "User is present" + ]; + + yield [ + <<<'JSON' + { + "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('4feb0ae3-7ffb-48dd-891e-c86b2cdeefd4')/mailboxSettings/automaticRepliesSetting", + "status": "scheduled", + "externalAudience": "none", + "internalReplyMessage": "Je suis en congé.", + "externalReplyMessage": "", + "scheduledStartDateTime": { + "dateTime": "2023-07-06T11:00:00.0000000", + "timeZone": "UTC" + }, + "scheduledEndDateTime": { + "dateTime": "2023-07-21T11:00:00.0000000", + "timeZone": "UTC" + } + } + JSON, + true, + 'User is absent with absence scheduled, we are within this period' + ]; + + yield [ + <<<'JSON' + { + "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('4feb0ae3-7ffb-48dd-891e-c86b2cdeefd4')/mailboxSettings/automaticRepliesSetting", + "status": "scheduled", + "externalAudience": "none", + "internalReplyMessage": "Je suis en congé.", + "externalReplyMessage": "", + "scheduledStartDateTime": { + "dateTime": "2023-07-08T11:00:00.0000000", + "timeZone": "UTC" + }, + "scheduledEndDateTime": { + "dateTime": "2023-07-21T11:00:00.0000000", + "timeZone": "UTC" + } + } + JSON, + false, + 'User is present: absence is scheduled for later' + ]; + + yield [ + <<<'JSON' + { + "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('4feb0ae3-7ffb-48dd-891e-c86b2cdeefd4')/mailboxSettings/automaticRepliesSetting", + "status": "scheduled", + "externalAudience": "none", + "internalReplyMessage": "Je suis en congé.", + "externalReplyMessage": "", + "scheduledStartDateTime": { + "dateTime": "2023-07-05T11:00:00.0000000", + "timeZone": "UTC" + }, + "scheduledEndDateTime": { + "dateTime": "2023-07-06T11:00:00.0000000", + "timeZone": "UTC" + } + } + JSON, + false, + 'User is present: absence is past' + ]; + + yield [ + <<<'JSON' + { + "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('4feb0ae3-7ffb-48dd-891e-c86b2cdeefd4')/mailboxSettings/automaticRepliesSetting", + "status": "alwaysEnabled", + "externalAudience": "none", + "internalReplyMessage": "Je suis en congé.", + "externalReplyMessage": "", + "scheduledStartDateTime": { + "dateTime": "2023-07-06T12:00:00.0000000", + "timeZone": "UTC" + }, + "scheduledEndDateTime": { + "dateTime": "2023-07-07T12:00:00.0000000", + "timeZone": "UTC" + } + } + JSON, + true, + "User is absent: absence is always enabled" + ]; + } + +} From 2861945a5251ed77d50bfe5a8e1fc6083234ba6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 6 Jul 2023 17:29:10 +0200 Subject: [PATCH 2/7] Syncer for user absence, from the msgraph reader --- .../Exception/UserAbsenceSyncException.php | 4 +- .../Connector/MSGraph/MSUserAbsenceReader.php | 2 +- .../MSGraph/MSUserAbsenceReaderInterface.php | 22 ++++++ .../Connector/MSGraph/MSUserAbsenceSync.php | 44 ++++++++++++ .../MSGraph/MSUserAbsenceSyncTest.php | 67 +++++++++++++++++++ 5 files changed, 136 insertions(+), 3 deletions(-) create mode 100644 src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReaderInterface.php create mode 100644 src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceSync.php create mode 100644 src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/MSUserAbsenceSyncTest.php diff --git a/src/Bundle/ChillCalendarBundle/Exception/UserAbsenceSyncException.php b/src/Bundle/ChillCalendarBundle/Exception/UserAbsenceSyncException.php index b46c93976..558774fc5 100644 --- a/src/Bundle/ChillCalendarBundle/Exception/UserAbsenceSyncException.php +++ b/src/Bundle/ChillCalendarBundle/Exception/UserAbsenceSyncException.php @@ -13,8 +13,8 @@ namespace Chill\CalendarBundle\Exception; class UserAbsenceSyncException extends \LogicException { - public function __construct(string $message = "", int $code = 20230706) + public function __construct(string $message = "", int $code = 20230706, ?\Throwable $previous = null) { - parent::__construct($message, $code); + parent::__construct($message, $code, $previous); } } diff --git a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReader.php b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReader.php index b79a55052..83376528f 100644 --- a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReader.php +++ b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReader.php @@ -21,7 +21,7 @@ use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface; use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; -readonly class MSUserAbsenceReader +final readonly class MSUserAbsenceReader implements MSUserAbsenceReaderInterface { public function __construct( private HttpClientInterface $machineHttpClient, diff --git a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReaderInterface.php b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReaderInterface.php new file mode 100644 index 000000000..a918bb7ea --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReaderInterface.php @@ -0,0 +1,22 @@ +absenceReader->isUserAbsent($user); + + if (null === $absence) { + return; + } + + if ($absence === $user->isAbsent()) { + // nothing to do + return; + } + + if ($absence) { + $user->setAbsenceStart($this->clock->now()); + } else { + $user->setAbsenceStart(null); + } + } +} diff --git a/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/MSUserAbsenceSyncTest.php b/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/MSUserAbsenceSyncTest.php new file mode 100644 index 000000000..ddd92086d --- /dev/null +++ b/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/MSUserAbsenceSyncTest.php @@ -0,0 +1,67 @@ +prophesize(MSUserAbsenceReaderInterface::class); + $userAbsenceReader->isUserAbsent($user)->willReturn($absenceFromMicrosoft); + + $clock = new MockClock(new \DateTimeImmutable('2023-07-01T12:00:00')); + + $syncer = new MSUserAbsenceSync($userAbsenceReader->reveal(), $clock); + + $syncer->syncUserAbsence($user); + + self::assertEquals($expectedAbsence, $user->isAbsent(), $message); + self::assertEquals($expectedAbsenceStart, $user->getAbsenceStart(), $message); + } + + public function provideDataTestSyncUserAbsence(): iterable + { + yield [new User(), false, false, null, "user present remains present"]; + yield [new User(), true, true, new \DateTimeImmutable('2023-07-01T12:00:00'), "user present becomes absent"]; + + $user = new User(); + $user->setAbsenceStart($abs = new \DateTimeImmutable("2023-07-01T12:00:00")); + yield [$user, true, true, $abs, "user absent remains absent"]; + + $user = new User(); + $user->setAbsenceStart($abs = new \DateTimeImmutable("2023-07-01T12:00:00")); + yield [$user, false, false, null, "user absent becomes present"]; + + yield [new User(), null, false, null, "user not syncable: presence do not change"]; + + $user = new User(); + $user->setAbsenceStart($abs = new \DateTimeImmutable("2023-07-01T12:00:00")); + yield [$user, null, true, $abs, "user not syncable: absence do not change"]; + } +} From 77d4b13c1bab2624a9b28c016f522486f859abb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 6 Jul 2023 21:33:01 +0200 Subject: [PATCH 3/7] Sync user absence / presence within MapAndSubscribeUserCalendarCommand --- .../MapAndSubscribeUserCalendarCommand.php | 182 ++++++++++-------- .../MSGraph/MSGraphUserRepository.php | 2 +- .../Connector/MSGraph/MSUserAbsenceReader.php | 3 +- .../Connector/MSGraph/MSUserAbsenceSync.php | 6 + .../MSGraph/MSUserAbsenceSyncTest.php | 3 +- 5 files changed, 109 insertions(+), 87 deletions(-) diff --git a/src/Bundle/ChillCalendarBundle/Command/MapAndSubscribeUserCalendarCommand.php b/src/Bundle/ChillCalendarBundle/Command/MapAndSubscribeUserCalendarCommand.php index b90fb83d4..193b04934 100644 --- a/src/Bundle/ChillCalendarBundle/Command/MapAndSubscribeUserCalendarCommand.php +++ b/src/Bundle/ChillCalendarBundle/Command/MapAndSubscribeUserCalendarCommand.php @@ -18,9 +18,12 @@ declare(strict_types=1); namespace Chill\CalendarBundle\Command; +use Chill\CalendarBundle\Exception\UserAbsenceSyncException; use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\EventsOnUserSubscriptionCreator; use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MapCalendarToUser; use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MSGraphUserRepository; +use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MSUserAbsenceSync; +use Chill\MainBundle\Repository\UserRepositoryInterface; use DateInterval; use DateTimeImmutable; use Doctrine\ORM\EntityManagerInterface; @@ -30,32 +33,17 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -class MapAndSubscribeUserCalendarCommand extends Command +final class MapAndSubscribeUserCalendarCommand extends Command { - private EntityManagerInterface $em; - - private EventsOnUserSubscriptionCreator $eventsOnUserSubscriptionCreator; - - private LoggerInterface $logger; - - private MapCalendarToUser $mapCalendarToUser; - - private MSGraphUserRepository $userRepository; - public function __construct( - EntityManagerInterface $em, - EventsOnUserSubscriptionCreator $eventsOnUserSubscriptionCreator, - LoggerInterface $logger, - MapCalendarToUser $mapCalendarToUser, - MSGraphUserRepository $userRepository + private readonly EntityManagerInterface $em, + private readonly EventsOnUserSubscriptionCreator $eventsOnUserSubscriptionCreator, + private readonly LoggerInterface $logger, + private readonly MapCalendarToUser $mapCalendarToUser, + private readonly UserRepositoryInterface $userRepository, + private readonly MSUserAbsenceSync $userAbsenceSync, ) { parent::__construct('chill:calendar:msgraph-user-map-subscribe'); - - $this->em = $em; - $this->eventsOnUserSubscriptionCreator = $eventsOnUserSubscriptionCreator; - $this->logger = $logger; - $this->mapCalendarToUser = $mapCalendarToUser; - $this->userRepository = $userRepository; } public function execute(InputInterface $input, OutputInterface $output): int @@ -67,83 +55,109 @@ class MapAndSubscribeUserCalendarCommand extends Command /** @var DateInterval $interval the interval before the end of the expiration */ $interval = new DateInterval('P1D'); $expiration = (new DateTimeImmutable('now'))->add(new DateInterval($input->getOption('subscription-duration'))); - $total = $this->userRepository->countByMostOldSubscriptionOrWithoutSubscriptionOrData($interval); + $users = $this->userRepository->findAllAsArray('fr'); $created = 0; $renewed = 0; - $this->logger->info(self::class . ' the number of user to get - renew', [ - 'total' => $total, + $this->logger->info(self::class . ' start user to get - renew', [ 'expiration' => $expiration->format(DateTimeImmutable::ATOM), ]); - while ($offset < $total) { - $users = $this->userRepository->findByMostOldSubscriptionOrWithoutSubscriptionOrData( - $interval, - $limit, - $offset - ); + foreach ($users as $u) { + ++$offset; - foreach ($users as $user) { - if (!$this->mapCalendarToUser->hasUserId($user)) { - $this->mapCalendarToUser->writeMetadata($user); - } - - if ($this->mapCalendarToUser->hasUserId($user)) { - // we first try to renew an existing subscription, if any. - // if not, or if it fails, we try to create a new one - if ($this->mapCalendarToUser->hasActiveSubscription($user)) { - $this->logger->debug(self::class . ' renew a subscription for', [ - 'userId' => $user->getId(), - 'username' => $user->getUsernameCanonical(), - ]); - - ['secret' => $secret, 'id' => $id, 'expiration' => $expirationTs] - = $this->eventsOnUserSubscriptionCreator->renewSubscriptionForUser($user, $expiration); - $this->mapCalendarToUser->writeSubscriptionMetadata($user, $expirationTs, $id, $secret); - - if (0 !== $expirationTs) { - ++$renewed; - } else { - $this->logger->warning(self::class . ' could not renew subscription for a user', [ - 'userId' => $user->getId(), - 'username' => $user->getUsernameCanonical(), - ]); - } - } - - if (!$this->mapCalendarToUser->hasActiveSubscription($user)) { - $this->logger->debug(self::class . ' create a subscription for', [ - 'userId' => $user->getId(), - 'username' => $user->getUsernameCanonical(), - ]); - - ['secret' => $secret, 'id' => $id, 'expiration' => $expirationTs] - = $this->eventsOnUserSubscriptionCreator->createSubscriptionForUser($user, $expiration); - $this->mapCalendarToUser->writeSubscriptionMetadata($user, $expirationTs, $id, $secret); - - if (0 !== $expirationTs) { - ++$created; - } else { - $this->logger->warning(self::class . ' could not create subscription for a user', [ - 'userId' => $user->getId(), - 'username' => $user->getUsernameCanonical(), - ]); - } - } - } - - ++$offset; + if (false === $u['enabled']) { + continue; } - $this->em->flush(); - $this->em->clear(); + $user = $this->userRepository->find($u['id']); + + if (null === $user) { + $this->logger->error("could not find user by id", ['uid' => $u['id']]); + $output->writeln("could not find user by id : " . $u['id']); + continue; + } + + if (!$this->mapCalendarToUser->hasUserId($user)) { + $user = $this->mapCalendarToUser->writeMetadata($user); + + // if user still does not have userid, continue + if (!$this->mapCalendarToUser->hasUserId($user)) { + $this->logger->warning("user does not have a counterpart on ms api", ['userId' => $user->getId(), 'email' => $user->getEmail()]); + $output->writeln(sprintf("giving up for user with email %s and id %s", $user->getEmail(), $user->getId())); + + continue; + } + } + + // sync user absence + try { + $this->userAbsenceSync->syncUserAbsence($user); + } catch (UserAbsenceSyncException $e) { + $this->logger->error("could not sync user absence", ['userId' => $user->getId(), 'email' => $user->getEmail(), 'exception' => $e->getTraceAsString(), "message" => $e->getMessage()]); + $output->writeln(sprintf("Could not sync user absence: id: %s and email: %s", $user->getId(), $user->getEmail())); + throw $e; + } + + // we first try to renew an existing subscription, if any. + // if not, or if it fails, we try to create a new one + if ($this->mapCalendarToUser->hasActiveSubscription($user)) { + $this->logger->debug(self::class . ' renew a subscription for', [ + 'userId' => $user->getId(), + 'username' => $user->getUsernameCanonical(), + ]); + + ['secret' => $secret, 'id' => $id, 'expiration' => $expirationTs] + = $this->eventsOnUserSubscriptionCreator->renewSubscriptionForUser($user, $expiration); + $this->mapCalendarToUser->writeSubscriptionMetadata($user, $expirationTs, $id, $secret); + + if (0 !== $expirationTs) { + ++$renewed; + } else { + $this->logger->warning(self::class . ' could not renew subscription for a user', [ + 'userId' => $user->getId(), + 'username' => $user->getUsernameCanonical(), + ]); + } + } + + if (!$this->mapCalendarToUser->hasActiveSubscription($user)) { + $this->logger->debug(self::class . ' create a subscription for', [ + 'userId' => $user->getId(), + 'username' => $user->getUsernameCanonical(), + ]); + + ['secret' => $secret, 'id' => $id, 'expiration' => $expirationTs] + = $this->eventsOnUserSubscriptionCreator->createSubscriptionForUser($user, $expiration); + $this->mapCalendarToUser->writeSubscriptionMetadata($user, $expirationTs, $id, $secret); + + if (0 !== $expirationTs) { + ++$created; + } else { + $this->logger->warning(self::class . ' could not create subscription for a user', [ + 'userId' => $user->getId(), + 'username' => $user->getUsernameCanonical(), + ]); + } + } + + + if (0 === $offset % $limit) { + $this->em->flush(); + $this->em->clear(); + } } + $this->em->flush(); + $this->em->clear(); + $this->logger->warning(self::class . ' process executed', [ 'created' => $created, 'renewed' => $renewed, ]); + $output->writeln("users synchronized"); + return 0; } @@ -152,7 +166,7 @@ class MapAndSubscribeUserCalendarCommand extends Command parent::configure(); $this - ->setDescription('MSGraph: collect user metadata and create subscription on events for users') + ->setDescription('MSGraph: collect user metadata and create subscription on events for users, and sync the user absence-presence') ->addOption( 'renew-before-end-interval', 'r', diff --git a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSGraphUserRepository.php b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSGraphUserRepository.php index c523a1e92..ae822669c 100644 --- a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSGraphUserRepository.php +++ b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSGraphUserRepository.php @@ -65,7 +65,7 @@ class MSGraphUserRepository } /** - * @return array|User[] + * @return array */ public function findByMostOldSubscriptionOrWithoutSubscriptionOrData(DateInterval $interval, int $limit = 50, int $offset = 0): array { diff --git a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReader.php b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReader.php index 83376528f..0e756c194 100644 --- a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReader.php +++ b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReader.php @@ -13,6 +13,7 @@ namespace Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph; use Chill\CalendarBundle\Exception\UserAbsenceSyncException; use Chill\MainBundle\Entity\User; +use Psr\Log\LoggerInterface; use Symfony\Component\Clock\ClockInterface; use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface; @@ -43,7 +44,7 @@ final readonly class MSUserAbsenceReader implements MSUserAbsenceReaderInterface try { $automaticRepliesSettings = $this->machineHttpClient - ->request('GET', '/users/' . $id . '/mailboxSettings/automaticRepliesSetting') + ->request('GET', 'users/' . $id . '/mailboxSettings/automaticRepliesSetting') ->toArray(true); } catch (ClientExceptionInterface|DecodingExceptionInterface|RedirectionExceptionInterface|TransportExceptionInterface $e) { throw new UserAbsenceSyncException("Error receiving response for mailboxSettings", 0, $e); diff --git a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceSync.php b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceSync.php index aaa06a64b..10bf21b9b 100644 --- a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceSync.php +++ b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceSync.php @@ -12,6 +12,7 @@ declare(strict_types=1); namespace Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph; use Chill\MainBundle\Entity\User; +use Psr\Log\LoggerInterface; use Symfony\Component\Clock\ClockInterface; readonly class MSUserAbsenceSync @@ -19,6 +20,7 @@ readonly class MSUserAbsenceSync public function __construct( private MSUserAbsenceReaderInterface $absenceReader, private ClockInterface $clock, + private LoggerInterface $logger, ) { } @@ -35,9 +37,13 @@ readonly class MSUserAbsenceSync return; } + $this->logger->info("will change user absence", ['userId' => $user->getId()]); + if ($absence) { + $this->logger->debug("make user absent", ['userId' => $user->getId()]); $user->setAbsenceStart($this->clock->now()); } else { + $this->logger->debug("make user present", ['userId' => $user->getId()]); $user->setAbsenceStart(null); } } diff --git a/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/MSUserAbsenceSyncTest.php b/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/MSUserAbsenceSyncTest.php index ddd92086d..1b0f1e416 100644 --- a/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/MSUserAbsenceSyncTest.php +++ b/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/MSUserAbsenceSyncTest.php @@ -17,6 +17,7 @@ use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MSUserAbsenceSync; use Chill\MainBundle\Entity\User; use PHPUnit\Framework\TestCase; use Prophecy\PhpUnit\ProphecyTrait; +use Psr\Log\NullLogger; use Symfony\Component\Clock\MockClock; /** @@ -37,7 +38,7 @@ class MSUserAbsenceSyncTest extends TestCase $clock = new MockClock(new \DateTimeImmutable('2023-07-01T12:00:00')); - $syncer = new MSUserAbsenceSync($userAbsenceReader->reveal(), $clock); + $syncer = new MSUserAbsenceSync($userAbsenceReader->reveal(), $clock, new NullLogger()); $syncer->syncUserAbsence($user); From 9b6e6ec20f6bf425245de6f1f08a21e46b7d4eae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 6 Jul 2023 21:34:43 +0200 Subject: [PATCH 4/7] add a changie --- .changes/unreleased/Feature-20230706-213428.yaml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changes/unreleased/Feature-20230706-213428.yaml diff --git a/.changes/unreleased/Feature-20230706-213428.yaml b/.changes/unreleased/Feature-20230706-213428.yaml new file mode 100644 index 000000000..092e5a2e0 --- /dev/null +++ b/.changes/unreleased/Feature-20230706-213428.yaml @@ -0,0 +1,5 @@ +kind: Feature +body: 'Sync user absence / presence through microsoft outlook / graph api. ' +time: 2023-07-06T21:34:28.973144334+02:00 +custom: + Issue: "124" From 93a598b5497339590a4936b5dae4036082d04de6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 6 Jul 2023 21:45:29 +0200 Subject: [PATCH 5/7] improve php applying rector rules --- .../ChillCalendarBundle/Exception/UserAbsenceSyncException.php | 2 +- .../RemoteCalendar/Connector/MSGraph/MSUserAbsenceReader.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Bundle/ChillCalendarBundle/Exception/UserAbsenceSyncException.php b/src/Bundle/ChillCalendarBundle/Exception/UserAbsenceSyncException.php index 558774fc5..a5e5a679a 100644 --- a/src/Bundle/ChillCalendarBundle/Exception/UserAbsenceSyncException.php +++ b/src/Bundle/ChillCalendarBundle/Exception/UserAbsenceSyncException.php @@ -13,7 +13,7 @@ namespace Chill\CalendarBundle\Exception; class UserAbsenceSyncException extends \LogicException { - public function __construct(string $message = "", int $code = 20230706, ?\Throwable $previous = null) + public function __construct(string $message = "", int $code = 20_230_706, ?\Throwable $previous = null) { parent::__construct($message, $code, $previous); } diff --git a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReader.php b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReader.php index 0e756c194..c70072a47 100644 --- a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReader.php +++ b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSUserAbsenceReader.php @@ -53,7 +53,7 @@ final readonly class MSUserAbsenceReader implements MSUserAbsenceReaderInterface } if (!array_key_exists("status", $automaticRepliesSettings)) { - throw new \LogicException("no key \"status\" on automatic replies settings: " . json_encode($automaticRepliesSettings)); + throw new \LogicException("no key \"status\" on automatic replies settings: " . json_encode($automaticRepliesSettings, JSON_THROW_ON_ERROR)); } return match ($automaticRepliesSettings['status']) { From d3251075e978331ed9b003d223e83bd95f2f8fc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 6 Jul 2023 21:55:29 +0200 Subject: [PATCH 6/7] fix loading of kernel if ms calendar is not created --- .../MSGraph/MSGraphUserRepository.php | 84 ------------------- .../RemoteCalendarCompilerPass.php | 16 ++-- 2 files changed, 6 insertions(+), 94 deletions(-) delete mode 100644 src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSGraphUserRepository.php diff --git a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSGraphUserRepository.php b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSGraphUserRepository.php deleted file mode 100644 index ae822669c..000000000 --- a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/MSGraphUserRepository.php +++ /dev/null @@ -1,84 +0,0 @@ -'msgraph' ?? 'subscription_events_expiration' - OR (attributes->'msgraph' ?? 'subscription_events_expiration' AND (attributes->'msgraph'->>'subscription_events_expiration')::int < EXTRACT(EPOCH FROM (NOW() + :interval::interval))) - LIMIT :limit OFFSET :offset - ; - SQL; - - private EntityManagerInterface $entityManager; - - public function __construct(EntityManagerInterface $entityManager) - { - $this->entityManager = $entityManager; - } - - public function countByMostOldSubscriptionOrWithoutSubscriptionOrData(DateInterval $interval): int - { - $rsm = new ResultSetMapping(); - $rsm->addScalarResult('c', 'c'); - - $sql = strtr(self::MOST_OLD_SUBSCRIPTION_OR_ANY_MS_GRAPH, [ - '{select}' => 'COUNT(u) AS c', - 'LIMIT :limit OFFSET :offset' => '', - ]); - - return $this->entityManager->createNativeQuery($sql, $rsm)->setParameters([ - 'interval' => $interval, - ])->getSingleScalarResult(); - } - - /** - * @return array - */ - public function findByMostOldSubscriptionOrWithoutSubscriptionOrData(DateInterval $interval, int $limit = 50, int $offset = 0): array - { - $rsm = new ResultSetMappingBuilder($this->entityManager); - $rsm->addRootEntityFromClassMetadata(User::class, 'u'); - - return $this->entityManager->createNativeQuery( - strtr(self::MOST_OLD_SUBSCRIPTION_OR_ANY_MS_GRAPH, ['{select}' => $rsm->generateSelectClause()]), - $rsm - )->setParameters([ - 'interval' => $interval, - 'limit' => $limit, - 'offset' => $offset, - ])->getResult(); - } -} diff --git a/src/Bundle/ChillCalendarBundle/RemoteCalendar/DependencyInjection/RemoteCalendarCompilerPass.php b/src/Bundle/ChillCalendarBundle/RemoteCalendar/DependencyInjection/RemoteCalendarCompilerPass.php index f56735de7..6a986c9db 100644 --- a/src/Bundle/ChillCalendarBundle/RemoteCalendar/DependencyInjection/RemoteCalendarCompilerPass.php +++ b/src/Bundle/ChillCalendarBundle/RemoteCalendar/DependencyInjection/RemoteCalendarCompilerPass.php @@ -23,6 +23,8 @@ use Chill\CalendarBundle\Command\MapAndSubscribeUserCalendarCommand; use Chill\CalendarBundle\Controller\RemoteCalendarConnectAzureController; use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MachineHttpClient; use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MachineTokenStorage; +use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MSUserAbsenceReaderInterface; +use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MSUserAbsenceSync; use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraphRemoteCalendarConnector; use Chill\CalendarBundle\RemoteCalendar\Connector\NullRemoteCalendarConnector; use Chill\CalendarBundle\RemoteCalendar\Connector\RemoteCalendarConnectorInterface; @@ -37,17 +39,13 @@ class RemoteCalendarCompilerPass implements CompilerPassInterface public function process(ContainerBuilder $container) { $config = $container->getParameter('chill_calendar'); - $connector = null; - if (!$config['remote_calendars_sync']['enabled']) { - $connector = NullRemoteCalendarConnector::class; - } - - if ($config['remote_calendars_sync']['microsoft_graph']['enabled']) { + if (true === $config['remote_calendars_sync']['microsoft_graph']['enabled']) { $connector = MSGraphRemoteCalendarConnector::class; $container->setAlias(HttpClientInterface::class . ' $machineHttpClient', MachineHttpClient::class); } else { + $connector = NullRemoteCalendarConnector::class; // remove services which cannot be loaded $container->removeDefinition(MapAndSubscribeUserCalendarCommand::class); $container->removeDefinition(AzureGrantAdminConsentAndAcquireToken::class); @@ -55,16 +53,14 @@ class RemoteCalendarCompilerPass implements CompilerPassInterface $container->removeDefinition(MachineTokenStorage::class); $container->removeDefinition(MachineHttpClient::class); $container->removeDefinition(MSGraphRemoteCalendarConnector::class); + $container->removeDefinition(MSUserAbsenceReaderInterface::class); + $container->removeDefinition(MSUserAbsenceSync::class); } if (!$container->hasAlias(Azure::class) && $container->hasDefinition('knpu.oauth2.client.azure')) { $container->setAlias(Azure::class, 'knpu.oauth2.provider.azure'); } - if (null === $connector) { - throw new RuntimeException('Could not configure remote calendar'); - } - foreach ([ NullRemoteCalendarConnector::class, MSGraphRemoteCalendarConnector::class, ] as $serviceId) { From 43b7139488dd0f3b99286b421ba5c1594b709d18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julien=20Fastr=C3=A9?= Date: Thu, 6 Jul 2023 22:01:45 +0200 Subject: [PATCH 7/7] One more changie [ci-skip] --- .changes/unreleased/Fixed-20230706-220125.yaml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changes/unreleased/Fixed-20230706-220125.yaml diff --git a/.changes/unreleased/Fixed-20230706-220125.yaml b/.changes/unreleased/Fixed-20230706-220125.yaml new file mode 100644 index 000000000..b9449d514 --- /dev/null +++ b/.changes/unreleased/Fixed-20230706-220125.yaml @@ -0,0 +1,6 @@ +kind: Fixed +body: 'Command to subscribe on MS Graph users calendars: improve the loop to be more + efficient' +time: 2023-07-06T22:01:25.847374805+02:00 +custom: + Issue: ""