Sync user absence / presence within MapAndSubscribeUserCalendarCommand

This commit is contained in:
Julien Fastré 2023-07-06 21:33:01 +02:00
parent 2861945a52
commit 77d4b13c1b
Signed by: julienfastre
GPG Key ID: BDE2190974723FCB
5 changed files with 109 additions and 87 deletions

View File

@ -18,9 +18,12 @@ declare(strict_types=1);
namespace Chill\CalendarBundle\Command; namespace Chill\CalendarBundle\Command;
use Chill\CalendarBundle\Exception\UserAbsenceSyncException;
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\EventsOnUserSubscriptionCreator; use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\EventsOnUserSubscriptionCreator;
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MapCalendarToUser; use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MapCalendarToUser;
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MSGraphUserRepository; use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MSGraphUserRepository;
use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MSUserAbsenceSync;
use Chill\MainBundle\Repository\UserRepositoryInterface;
use DateInterval; use DateInterval;
use DateTimeImmutable; use DateTimeImmutable;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
@ -30,32 +33,17 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface; 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( public function __construct(
EntityManagerInterface $em, private readonly EntityManagerInterface $em,
EventsOnUserSubscriptionCreator $eventsOnUserSubscriptionCreator, private readonly EventsOnUserSubscriptionCreator $eventsOnUserSubscriptionCreator,
LoggerInterface $logger, private readonly LoggerInterface $logger,
MapCalendarToUser $mapCalendarToUser, private readonly MapCalendarToUser $mapCalendarToUser,
MSGraphUserRepository $userRepository private readonly UserRepositoryInterface $userRepository,
private readonly MSUserAbsenceSync $userAbsenceSync,
) { ) {
parent::__construct('chill:calendar:msgraph-user-map-subscribe'); 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 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 */ /** @var DateInterval $interval the interval before the end of the expiration */
$interval = new DateInterval('P1D'); $interval = new DateInterval('P1D');
$expiration = (new DateTimeImmutable('now'))->add(new DateInterval($input->getOption('subscription-duration'))); $expiration = (new DateTimeImmutable('now'))->add(new DateInterval($input->getOption('subscription-duration')));
$total = $this->userRepository->countByMostOldSubscriptionOrWithoutSubscriptionOrData($interval); $users = $this->userRepository->findAllAsArray('fr');
$created = 0; $created = 0;
$renewed = 0; $renewed = 0;
$this->logger->info(self::class . ' the number of user to get - renew', [ $this->logger->info(self::class . ' start user to get - renew', [
'total' => $total,
'expiration' => $expiration->format(DateTimeImmutable::ATOM), 'expiration' => $expiration->format(DateTimeImmutable::ATOM),
]); ]);
while ($offset < $total) { foreach ($users as $u) {
$users = $this->userRepository->findByMostOldSubscriptionOrWithoutSubscriptionOrData( ++$offset;
$interval,
$limit,
$offset
);
foreach ($users as $user) { if (false === $u['enabled']) {
if (!$this->mapCalendarToUser->hasUserId($user)) { continue;
$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;
} }
$this->em->flush(); $user = $this->userRepository->find($u['id']);
$this->em->clear();
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', [ $this->logger->warning(self::class . ' process executed', [
'created' => $created, 'created' => $created,
'renewed' => $renewed, 'renewed' => $renewed,
]); ]);
$output->writeln("users synchronized");
return 0; return 0;
} }
@ -152,7 +166,7 @@ class MapAndSubscribeUserCalendarCommand extends Command
parent::configure(); parent::configure();
$this $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( ->addOption(
'renew-before-end-interval', 'renew-before-end-interval',
'r', 'r',

View File

@ -65,7 +65,7 @@ class MSGraphUserRepository
} }
/** /**
* @return array|User[] * @return array<User>
*/ */
public function findByMostOldSubscriptionOrWithoutSubscriptionOrData(DateInterval $interval, int $limit = 50, int $offset = 0): array public function findByMostOldSubscriptionOrWithoutSubscriptionOrData(DateInterval $interval, int $limit = 50, int $offset = 0): array
{ {

View File

@ -13,6 +13,7 @@ namespace Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph;
use Chill\CalendarBundle\Exception\UserAbsenceSyncException; use Chill\CalendarBundle\Exception\UserAbsenceSyncException;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Psr\Log\LoggerInterface;
use Symfony\Component\Clock\ClockInterface; use Symfony\Component\Clock\ClockInterface;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface; use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
@ -43,7 +44,7 @@ final readonly class MSUserAbsenceReader implements MSUserAbsenceReaderInterface
try { try {
$automaticRepliesSettings = $this->machineHttpClient $automaticRepliesSettings = $this->machineHttpClient
->request('GET', '/users/' . $id . '/mailboxSettings/automaticRepliesSetting') ->request('GET', 'users/' . $id . '/mailboxSettings/automaticRepliesSetting')
->toArray(true); ->toArray(true);
} catch (ClientExceptionInterface|DecodingExceptionInterface|RedirectionExceptionInterface|TransportExceptionInterface $e) { } catch (ClientExceptionInterface|DecodingExceptionInterface|RedirectionExceptionInterface|TransportExceptionInterface $e) {
throw new UserAbsenceSyncException("Error receiving response for mailboxSettings", 0, $e); throw new UserAbsenceSyncException("Error receiving response for mailboxSettings", 0, $e);

View File

@ -12,6 +12,7 @@ declare(strict_types=1);
namespace Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph; namespace Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use Psr\Log\LoggerInterface;
use Symfony\Component\Clock\ClockInterface; use Symfony\Component\Clock\ClockInterface;
readonly class MSUserAbsenceSync readonly class MSUserAbsenceSync
@ -19,6 +20,7 @@ readonly class MSUserAbsenceSync
public function __construct( public function __construct(
private MSUserAbsenceReaderInterface $absenceReader, private MSUserAbsenceReaderInterface $absenceReader,
private ClockInterface $clock, private ClockInterface $clock,
private LoggerInterface $logger,
) { ) {
} }
@ -35,9 +37,13 @@ readonly class MSUserAbsenceSync
return; return;
} }
$this->logger->info("will change user absence", ['userId' => $user->getId()]);
if ($absence) { if ($absence) {
$this->logger->debug("make user absent", ['userId' => $user->getId()]);
$user->setAbsenceStart($this->clock->now()); $user->setAbsenceStart($this->clock->now());
} else { } else {
$this->logger->debug("make user present", ['userId' => $user->getId()]);
$user->setAbsenceStart(null); $user->setAbsenceStart(null);
} }
} }

View File

@ -17,6 +17,7 @@ use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\MSUserAbsenceSync;
use Chill\MainBundle\Entity\User; use Chill\MainBundle\Entity\User;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\PhpUnit\ProphecyTrait;
use Psr\Log\NullLogger;
use Symfony\Component\Clock\MockClock; use Symfony\Component\Clock\MockClock;
/** /**
@ -37,7 +38,7 @@ class MSUserAbsenceSyncTest extends TestCase
$clock = new MockClock(new \DateTimeImmutable('2023-07-01T12:00:00')); $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); $syncer->syncUserAbsence($user);