258 lines
9.0 KiB
PHP

<?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);
/*
* 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.
*/
namespace Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph;
use Chill\CalendarBundle\Entity\Calendar;
use Chill\CalendarBundle\Entity\CalendarRange;
use Chill\CalendarBundle\Entity\Invite;
use Chill\CalendarBundle\RemoteCalendar\Model\RemoteEvent;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Templating\Entity\PersonRenderInterface;
use Psr\Log\LoggerInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* Convert Chill Calendar event to Remote MS Graph event, and MS Graph
* event to RemoteEvent.
*/
class RemoteEventConverter
{
/**
* valid when the remote string contains also a timezone, like in
* lastModifiedDate.
*/
final public const REMOTE_DATETIMEZONE_FORMAT = 'Y-m-d\TH:i:s.u?P';
/**
* Same as above, but sometimes the date is expressed with only 6 milliseconds.
*/
final public const REMOTE_DATETIMEZONE_FORMAT_ALT = 'Y-m-d\TH:i:s.uP';
private const REMOTE_DATE_FORMAT = 'Y-m-d\TH:i:s.u0';
private const REMOTE_DATETIME_WITHOUT_TZ_FORMAT = 'Y-m-d\TH:i:s.u?';
private readonly \DateTimeZone $defaultDateTimeZone;
private readonly \DateTimeZone $remoteDateTimeZone;
public function __construct(
private readonly \Twig\Environment $engine,
private readonly LocationConverter $locationConverter,
private readonly LoggerInterface $logger,
private readonly PersonRenderInterface $personRender,
private readonly TranslatorInterface $translator,
) {
$this->defaultDateTimeZone = (new \DateTimeImmutable())->getTimezone();
$this->remoteDateTimeZone = self::getRemoteTimeZone();
}
/**
* Transform a CalendarRange into a representation suitable for storing into MSGraph.
*
* @return array an array representation for event in MS Graph
*/
public function calendarRangeToEvent(CalendarRange $calendarRange): array
{
return [
'subject' => $this->translator->trans('remote_calendar.calendar_range_title'),
'start' => [
'dateTime' => $calendarRange->getStartDate()->setTimezone($this->remoteDateTimeZone)
->format(self::REMOTE_DATE_FORMAT),
'timeZone' => 'UTC',
],
'end' => [
'dateTime' => $calendarRange->getEndDate()->setTimezone($this->remoteDateTimeZone)
->format(self::REMOTE_DATE_FORMAT),
'timeZone' => 'UTC',
],
'attendees' => [
[
'emailAddress' => [
'address' => $calendarRange->getUser()->getEmailCanonical(),
'name' => $calendarRange->getUser()->getLabel(),
],
],
],
'isReminderOn' => false,
'location' => $this->locationConverter->locationToRemote($calendarRange->getLocation()),
];
}
public function calendarToEvent(Calendar $calendar): array
{
$result = array_merge(
[
'subject' => '[Chill] '.
implode(
', ',
$calendar->getPersons()->map(fn (Person $p) => $this->personRender->renderString($p, []))->toArray()
),
'start' => [
'dateTime' => $calendar->getStartDate()->setTimezone($this->remoteDateTimeZone)
->format(self::REMOTE_DATE_FORMAT),
'timeZone' => 'UTC',
],
'end' => [
'dateTime' => $calendar->getEndDate()->setTimezone($this->remoteDateTimeZone)
->format(self::REMOTE_DATE_FORMAT),
'timeZone' => 'UTC',
],
'allowNewTimeProposals' => false,
'transactionId' => 'calendar_'.$calendar->getId(),
'body' => [
'contentType' => 'text',
'content' => $this->engine->render(
'@ChillCalendar/MSGraph/calendar_event_body.html.twig',
['calendar' => $calendar]
),
],
'responseRequested' => true,
'isReminderOn' => false,
],
$this->calendarToEventAttendeesOnly($calendar)
);
if ($calendar->hasLocation()) {
$result['location'] = $this->locationConverter->locationToRemote($calendar->getLocation());
}
return $result;
}
public function calendarToEventAttendeesOnly(Calendar $calendar): array
{
return [
'attendees' => $calendar->getInvites()->map(
fn (Invite $i) => $this->buildInviteToAttendee($i)
)->toArray(),
];
}
public function convertAvailabilityToRemoteEvent(array $event): RemoteEvent
{
$startDate =
\DateTimeImmutable::createFromFormat(self::REMOTE_DATE_FORMAT, $event['start']['dateTime'], $this->remoteDateTimeZone)
->setTimezone($this->defaultDateTimeZone);
$endDate =
\DateTimeImmutable::createFromFormat(self::REMOTE_DATE_FORMAT, $event['end']['dateTime'], $this->remoteDateTimeZone)
->setTimezone($this->defaultDateTimeZone);
return new RemoteEvent(
uniqid('generated_'),
$this->translator->trans('remote_ms_graph.freebusy_statuses.'.$event['status']),
'',
$startDate,
$endDate
);
}
public static function convertStringDateWithoutTimezone(string $date): \DateTimeImmutable
{
$d = \DateTimeImmutable::createFromFormat(
self::REMOTE_DATETIME_WITHOUT_TZ_FORMAT,
$date,
self::getRemoteTimeZone()
);
if (false === $d) {
throw new \RuntimeException("could not convert string date to datetime: {$date}");
}
return $d->setTimezone((new \DateTimeImmutable())->getTimezone());
}
public static function convertStringDateWithTimezone(string $date): \DateTimeImmutable
{
$d = \DateTimeImmutable::createFromFormat(self::REMOTE_DATETIMEZONE_FORMAT, $date);
if (false === $d) {
throw new \RuntimeException("could not convert string date to datetime: {$date}");
}
$d->setTimezone((new \DateTimeImmutable())->getTimezone());
return $d;
}
public function convertToRemote(array $event): RemoteEvent
{
$startDate =
\DateTimeImmutable::createFromFormat(self::REMOTE_DATE_FORMAT, $event['start']['dateTime'], $this->remoteDateTimeZone)
->setTimezone($this->defaultDateTimeZone);
$endDate =
\DateTimeImmutable::createFromFormat(self::REMOTE_DATE_FORMAT, $event['end']['dateTime'], $this->remoteDateTimeZone)
->setTimezone($this->defaultDateTimeZone);
return new RemoteEvent(
$event['id'],
$event['subject'],
'',
$startDate,
$endDate,
$event['isAllDay']
);
}
public function getLastModifiedDate(array $event): \DateTimeImmutable
{
$date = \DateTimeImmutable::createFromFormat(self::REMOTE_DATETIMEZONE_FORMAT, $event['lastModifiedDateTime']);
if (false === $date) {
$date = \DateTimeImmutable::createFromFormat(self::REMOTE_DATETIMEZONE_FORMAT_ALT, $event['lastModifiedDateTime']);
}
if (false === $date) {
$this->logger->error(self::class.' Could not convert lastModifiedDate', [
'actual' => $event['lastModifiedDateTime'],
'format' => self::REMOTE_DATETIMEZONE_FORMAT,
'format_alt' => self::REMOTE_DATETIMEZONE_FORMAT_ALT,
]);
throw new \RuntimeException(sprintf('could not convert lastModifiedDate: %s, expected format: %s', $event['lastModifiedDateTime'], self::REMOTE_DATETIMEZONE_FORMAT.' and '.self::REMOTE_DATETIMEZONE_FORMAT_ALT));
}
return $date;
}
/**
* Return a string which format a DateTime to string. To be used in POST requests,.
*/
public static function getRemoteDateTimeSimpleFormat(): string
{
return 'Y-m-d\TH:i:s';
}
public static function getRemoteTimeZone(): \DateTimeZone
{
return new \DateTimeZone('UTC');
}
private function buildInviteToAttendee(Invite $invite): array
{
return [
'emailAddress' => [
'address' => $invite->getUser()->getEmail(),
'name' => $invite->getUser()->getLabel(),
],
'type' => 'Required',
];
}
}