some data in msgraph synchro and answer on calendar invite

This commit is contained in:
Julien Fastré 2022-05-26 19:25:59 +02:00
parent 59a64e9a62
commit 7c0bdc5abe
7 changed files with 433 additions and 257 deletions

View File

@ -0,0 +1,69 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\CalendarBundle\Controller;
use Chill\CalendarBundle\Entity\Calendar;
use Chill\CalendarBundle\Entity\Invite;
use Chill\CalendarBundle\Security\Voter\InviteVoter;
use Chill\MainBundle\Entity\User;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
use function in_array;
class InviteApiController
{
private EntityManagerInterface $entityManager;
private Security $security;
public function __construct(Security $security, EntityManagerInterface $entityManager)
{
$this->security = $security;
$this->entityManager = $entityManager;
}
/**
* Give an answer to a calendar invite.
*
* @Route("/api/1.0/calendar/calendar/{id}/answer/{answer}.json", methods={"post"})
*/
public function answer(Calendar $calendar, string $answer): Response
{
$user = $this->security->getUser();
if (!$user instanceof User) {
throw new AccessDeniedHttpException('not a regular user');
}
if (null === $invite = $calendar->getInviteForUser($user)) {
throw new AccessDeniedHttpException('not invited to this calendar');
}
if (!$this->security->isGranted(InviteVoter::ANSWER, $invite)) {
throw new AccessDeniedHttpException('not allowed to answer on this invitation');
}
if (!in_array($answer, Invite::STATUSES, true) || Invite::PENDING === $answer) {
throw new BadRequestHttpException('answer not valid');
}
$invite->setStatus($answer);
$this->entityManager->flush();
return new JsonResponse(null, Response::HTTP_ACCEPTED, [], false);
}
}

View File

@ -26,6 +26,7 @@ use DateInterval;
use DateTimeImmutable; use DateTimeImmutable;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use LogicException; use LogicException;
use Symfony\Component\Serializer\Annotation as Serializer; use Symfony\Component\Serializer\Annotation as Serializer;
@ -212,7 +213,7 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface
public function addUser(User $user): self public function addUser(User $user): self
{ {
if (!$this->getUsers()->contains($user)) { if (!$this->getUsers()->contains($user) && $this->getMainUser() !== $user) {
$this->addInvite((new Invite())->setUser($user)); $this->addInvite((new Invite())->setUser($user));
} }
@ -263,6 +264,21 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface
return $this->id; return $this->id;
} }
public function getInviteForUser(User $user): ?Invite
{
$criteria = Criteria::create();
$criteria->where(Criteria::expr()->eq('user', $user));
$matchings = $this->invites
->matching($criteria);
if (1 === $matchings->count()) {
return $matchings->first();
}
return null;
}
/** /**
* @return Collection|Invite[] * @return Collection|Invite[]
*/ */
@ -365,6 +381,18 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface
return null !== $this->calendarRange; return null !== $this->calendarRange;
} }
/**
* return true if the user is invited.
*/
public function isInvited(User $user): bool
{
if ($this->getMainUser() === $user) {
return false;
}
return $this->getUsers()->contains($user);
}
public static function loadValidatorMetadata(ClassMetadata $metadata): void public static function loadValidatorMetadata(ClassMetadata $metadata): void
{ {
$metadata->addPropertyConstraint('startDate', new NotBlank()); $metadata->addPropertyConstraint('startDate', new NotBlank());
@ -485,6 +513,7 @@ class Calendar implements TrackCreationInterface, TrackUpdateInterface
} }
$this->mainUser = $mainUser; $this->mainUser = $mainUser;
$this->removeUser($mainUser);
return $this; return $this;
} }

View File

@ -36,6 +36,16 @@ class Invite implements TrackUpdateInterface, TrackCreationInterface
public const PENDING = 'pending'; public const PENDING = 'pending';
/**
* all statuses in one const.
*/
public const STATUSES = [
self::ACCEPTED,
self::DECLINED,
self::PENDING,
self::TENTATIVELY_ACCEPTED,
];
public const TENTATIVELY_ACCEPTED = 'tentative'; public const TENTATIVELY_ACCEPTED = 'tentative';
/** /**

View File

@ -21,7 +21,6 @@ use DateTimeImmutable;
use DateTimeZone; use DateTimeZone;
use Symfony\Component\Templating\EngineInterface; use Symfony\Component\Templating\EngineInterface;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
use Twig\Environment;
/** /**
* Convert Chill Calendar event to Remote MS Graph event, and MS Graph * Convert Chill Calendar event to Remote MS Graph event, and MS Graph
@ -35,14 +34,14 @@ class RemoteEventConverter
private DateTimeZone $defaultDateTimeZone; private DateTimeZone $defaultDateTimeZone;
private EngineInterface $engine;
private PersonRenderInterface $personRender; private PersonRenderInterface $personRender;
private DateTimeZone $remoteDateTimeZone; private DateTimeZone $remoteDateTimeZone;
private TranslatorInterface $translator; private TranslatorInterface $translator;
private EngineInterface $engine;
public function __construct(EngineInterface $engine, PersonRenderInterface $personRender, TranslatorInterface $translator) public function __construct(EngineInterface $engine, PersonRenderInterface $personRender, TranslatorInterface $translator)
{ {
$this->engine = $engine; $this->engine = $engine;
@ -112,7 +111,7 @@ class RemoteEventConverter
'@ChillCalendar/MSGraph/calendar_event_body.html.twig', '@ChillCalendar/MSGraph/calendar_event_body.html.twig',
['calendar' => $calendar] ['calendar' => $calendar]
), ),
] ],
], ],
$this->calendarToEventAttendeesOnly($calendar) $this->calendarToEventAttendeesOnly($calendar)
); );
@ -129,17 +128,6 @@ class RemoteEventConverter
]; ];
} }
private function buildInviteToAttendee(Invite $invite): array
{
return [
'emailAddress' => [
'address' => $invite->getUser()->getEmail(),
'name' => $invite->getUser()->getLabel(),
],
'type' => 'Required',
];
}
public function convertAvailabilityToRemoteEvent(array $event): RemoteEvent public function convertAvailabilityToRemoteEvent(array $event): RemoteEvent
{ {
$startDate = $startDate =
@ -193,4 +181,15 @@ class RemoteEventConverter
{ {
return new DateTimeZone('UTC'); return new DateTimeZone('UTC');
} }
private function buildInviteToAttendee(Invite $invite): array
{
return [
'emailAddress' => [
'address' => $invite->getUser()->getEmail(),
'name' => $invite->getUser()->getLabel(),
],
'type' => 'Required',
];
}
} }

View File

@ -28,6 +28,7 @@ use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
use function count;
class MSGraphRemoteCalendarConnector implements RemoteCalendarConnectorInterface class MSGraphRemoteCalendarConnector implements RemoteCalendarConnectorInterface
{ {
@ -41,10 +42,10 @@ class MSGraphRemoteCalendarConnector implements RemoteCalendarConnectorInterface
private RemoteEventConverter $remoteEventConverter; private RemoteEventConverter $remoteEventConverter;
private TranslatorInterface $translator;
private OnBehalfOfUserTokenStorage $tokenStorage; private OnBehalfOfUserTokenStorage $tokenStorage;
private TranslatorInterface $translator;
private UrlGeneratorInterface $urlGenerator; private UrlGeneratorInterface $urlGenerator;
private OnBehalfOfUserHttpClient $userHttpClient; private OnBehalfOfUserHttpClient $userHttpClient;
@ -189,10 +190,9 @@ class MSGraphRemoteCalendarConnector implements RemoteCalendarConnectorInterface
->addRemoteAttributes([ ->addRemoteAttributes([
'lastModifiedDateTime' => null, 'lastModifiedDateTime' => null,
'changeKey' => null, 'changeKey' => null,
'previousId' => $calendar->getCalendarRange()->getRemoteId() 'previousId' => $calendar->getCalendarRange()->getRemoteId(),
]) ])
->setRemoteId('') ->setRemoteId('');
;
} }
if (null !== $previousCalendarRange) { if (null !== $previousCalendarRange) {
@ -209,31 +209,30 @@ class MSGraphRemoteCalendarConnector implements RemoteCalendarConnectorInterface
} }
} }
private function patchCalendarOnRemote(Calendar $calendar, array $newInvites): void private function cancelOnRemote(string $remoteId, string $comment, User $user, string $identifier): void
{ {
$eventDatas = []; $userId = $this->mapCalendarToUser->getUserId($user);
$eventDatas[] = $this->remoteEventConverter->calendarToEvent($calendar);
if (0 < count($newInvites)) { if (null === $userId) {
$eventDatas[] = $this->remoteEventConverter->calendarToEventAttendeesOnly($calendar); return;
} }
foreach ($eventDatas as $eventData) { try {
$this->machineHttpClient->request(
'POST',
"users/{$userId}/calendar/events/{$remoteId}/cancel",
[ [
'id' => $id, 'json' => ['Comment' => $comment],
'lastModifiedDateTime' => $lastModified, ]
'changeKey' => $changeKey
] = $this->patchOnRemote(
$calendar->getRemoteId(),
$eventData,
$calendar->getMainUser(),
'calendar_'.$calendar->getId()
); );
} catch (ClientExceptionInterface $e) {
$calendar->addRemoteAttributes([ $this->logger->warning('could not update calendar range to remote', [
'lastModifiedDateTime' => $lastModified, 'exception' => $e->getTraceAsString(),
'changeKey' => $changeKey, 'content' => $e->getResponse()->getContent(),
'calendarRangeId' => $identifier,
]); ]);
throw $e;
} }
} }
@ -330,7 +329,7 @@ class MSGraphRemoteCalendarConnector implements RemoteCalendarConnectorInterface
$calendarRange->setRemoteId($id) $calendarRange->setRemoteId($id)
->addRemoteAttributes([ ->addRemoteAttributes([
'lastModifiedDateTime' => $lastModified, 'lastModifiedDateTime' => $lastModified,
'changeKey' => $changeKey 'changeKey' => $changeKey,
]); ]);
} }
@ -404,33 +403,32 @@ class MSGraphRemoteCalendarConnector implements RemoteCalendarConnectorInterface
); );
} }
private function cancelOnRemote(string $remoteId, string $comment, User $user, string $identifier): void private function patchCalendarOnRemote(Calendar $calendar, array $newInvites): void
{ {
$userId = $this->mapCalendarToUser->getUserId($user); $eventDatas = [];
$eventDatas[] = $this->remoteEventConverter->calendarToEvent($calendar);
if (null === $userId) { if (0 < count($newInvites)) {
return; $eventDatas[] = $this->remoteEventConverter->calendarToEventAttendeesOnly($calendar);
} }
try { foreach ($eventDatas as $eventData) {
$this->machineHttpClient->request(
'POST',
"users/{$userId}/calendar/events/{$remoteId}/cancel",
[ [
'json' => ['Comment' => $comment] 'id' => $id,
] 'lastModifiedDateTime' => $lastModified,
'changeKey' => $changeKey
] = $this->patchOnRemote(
$calendar->getRemoteId(),
$eventData,
$calendar->getMainUser(),
'calendar_' . $calendar->getId()
); );
} catch (ClientExceptionInterface $e) {
$this->logger->warning('could not update calendar range to remote', [ $calendar->addRemoteAttributes([
'exception' => $e->getTraceAsString(), 'lastModifiedDateTime' => $lastModified,
'content' => $e->getResponse()->getContent(), 'changeKey' => $changeKey,
'calendarRangeId' => $identifier,
]); ]);
throw $e;
} }
} }
/** /**

View File

@ -0,0 +1,35 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\CalendarBundle\Security\Voter;
use Chill\CalendarBundle\Entity\Invite;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
class InviteVoter extends Voter
{
public const ANSWER = 'CHILL_CALENDAR_INVITE_ANSWER';
protected function supports($attribute, $subject): bool
{
return $subject instanceof Invite && self::ANSWER === $attribute;
}
/**
* @param string $attribute
* @param Invite $subject
*/
protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
{
return $token->getUser() === $subject->getUser();
}
}

View File

@ -1,12 +1,12 @@
--- ---
openapi: "3.0.0" #openapi: "3.0.0"
info: #info:
version: "1.0.0" # version: "1.0.0"
title: "Chill api" # title: "Chill api"
description: "Api documentation for chill. Currently, work in progress" # description: "Api documentation for chill. Currently, work in progress"
servers: #servers:
- url: "/api" # - url: "/api"
description: "Your current dev server" # description: "Your current dev server"
components: components:
schemas: schemas:
@ -31,6 +31,42 @@ components:
type: string type: string
paths: paths:
/1.0/calendar/calendar/{id}/answer/{answer}.json:
post:
tags:
- calendar
summary: Answer to a calendar's invite
parameters:
-
in: path
name: id
required: true
description: the calendar id
schema:
type: integer
format: integer
minimum: 0
-
in: path
name: answer
required: true
description: the answer
schema:
type: string
enum:
- accepted
- declined
- tentative
responses:
400:
description: bad answer
403:
description: not invited
404:
description: not found
202:
description: accepted
/1.0/calendar/calendar.json: /1.0/calendar/calendar.json:
get: get:
tags: tags: