mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2026-03-13 01:17:45 +00:00
Create a connector to synchronize with zimbra calendars
This commit is contained in:
@@ -47,6 +47,8 @@ class ChillCalendarExtension extends Extension implements PrependExtensionInterf
|
||||
} else {
|
||||
$container->setParameter('chill_calendar.short_messages', null);
|
||||
}
|
||||
|
||||
$container->setParameter('chill_calendar.remote_calendar_dsn', $config['remote_calendar_dsn']);
|
||||
}
|
||||
|
||||
public function prepend(ContainerBuilder $container)
|
||||
|
||||
@@ -32,9 +32,10 @@ class Configuration implements ConfigurationInterface
|
||||
->canBeDisabled()
|
||||
->children()->end()
|
||||
->end() // end for short_messages
|
||||
->scalarNode('remote_calendar_dsn')->defaultValue('null://null')->cannotBeEmpty()->end()
|
||||
->arrayNode('remote_calendars_sync')->canBeEnabled()
|
||||
->children()
|
||||
->arrayNode('microsoft_graph')->canBeEnabled()
|
||||
->arrayNode('microsoft_graph')->canBeEnabled()->setDeprecated('chill-project/chill-bundles', '4.7.0', 'The child node %node% at path %path% is deprecated: use remote_calendar_dsn instead, with a "msgraph://default" value')
|
||||
->children()
|
||||
->end() // end of machine_access_token
|
||||
->end() // end of microsoft_graph children
|
||||
|
||||
@@ -107,6 +107,11 @@ class CalendarRange implements TrackCreationInterface, TrackUpdateInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function hasLocation(): bool
|
||||
{
|
||||
return null !== $this->location;
|
||||
}
|
||||
|
||||
public function setLocation(?Location $location): self
|
||||
{
|
||||
$this->location = $location;
|
||||
|
||||
@@ -22,6 +22,7 @@ use Chill\CalendarBundle\Messenger\Message\CalendarRemovedMessage;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Connector\RemoteCalendarConnectorInterface;
|
||||
use Chill\CalendarBundle\Repository\CalendarRangeRepository;
|
||||
use Chill\MainBundle\Repository\UserRepositoryInterface;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
|
||||
|
||||
/**
|
||||
@@ -31,7 +32,12 @@ use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
|
||||
*/
|
||||
class CalendarRemoveHandler implements MessageHandlerInterface
|
||||
{
|
||||
public function __construct(private readonly RemoteCalendarConnectorInterface $remoteCalendarConnector, private readonly CalendarRangeRepository $calendarRangeRepository, private readonly UserRepositoryInterface $userRepository) {}
|
||||
public function __construct(
|
||||
private readonly RemoteCalendarConnectorInterface $remoteCalendarConnector,
|
||||
private readonly CalendarRangeRepository $calendarRangeRepository,
|
||||
private readonly UserRepositoryInterface $userRepository,
|
||||
private readonly EntityManagerInterface $entityManager,
|
||||
) {}
|
||||
|
||||
public function __invoke(CalendarRemovedMessage $message)
|
||||
{
|
||||
@@ -47,5 +53,7 @@ class CalendarRemoveHandler implements MessageHandlerInterface
|
||||
$this->userRepository->find($message->getCalendarUserId()),
|
||||
$associatedRange
|
||||
);
|
||||
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,42 +21,137 @@ namespace Chill\CalendarBundle\RemoteCalendar\Connector;
|
||||
use Chill\CalendarBundle\Entity\Calendar;
|
||||
use Chill\CalendarBundle\Entity\CalendarRange;
|
||||
use Chill\CalendarBundle\Entity\Invite;
|
||||
use Chill\CalendarBundle\Messenger\Message\CalendarMessage;
|
||||
use Chill\CalendarBundle\RemoteCalendar\Model\RemoteEvent;
|
||||
use Chill\MainBundle\Entity\User;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* Contract for connectors that synchronize Chill calendars with a remote
|
||||
* calendar provider (for example Microsoft 365/Graph, Zimbra, ...).
|
||||
*
|
||||
* Implementations act as an adapter between Chill domain objects
|
||||
* (Calendar, CalendarRange, Invite) and the remote provider API. They must:
|
||||
* - expose a readiness flow for per-user authorization when applicable
|
||||
* (see {@see getMakeReadyResponse()} and {@see isReady()});
|
||||
* - list and count remote events in a time range for a given user;
|
||||
* - mirror local lifecycle changes to the remote provider for calendars,
|
||||
* calendar ranges (availability/busy blocks) and invites/attendees.
|
||||
*
|
||||
* Use {@see MSGraphRemoteCalendarConnector} as a reference implementation for
|
||||
* expected behaviours, error handling and parameter semantics.
|
||||
*/
|
||||
interface RemoteCalendarConnectorInterface
|
||||
{
|
||||
public function countEventsForUser(User $user, \DateTimeImmutable $startDate, \DateTimeImmutable $endDate): int;
|
||||
|
||||
/**
|
||||
* Return a response, more probably a RedirectResponse, where the user
|
||||
* will be able to fullfill requirements to prepare this connector and
|
||||
* make it ready.
|
||||
* Returns a Response (typically a RedirectResponse) that lets the current
|
||||
* user perform the steps required to make the connector usable (for
|
||||
* example, OAuth consent or account linking). After completion, the user
|
||||
* should be redirected back to the given path.
|
||||
*/
|
||||
public function getMakeReadyResponse(string $returnPath): Response;
|
||||
|
||||
/**
|
||||
* Return true if the connector is ready to act as a proxy for reading
|
||||
* remote calendars.
|
||||
* Returns true when the connector is ready to access the remote provider
|
||||
* on behalf of the current user (e.g. required tokens/consent exist).
|
||||
*/
|
||||
public function isReady(): bool;
|
||||
|
||||
/**
|
||||
* Lists events from the remote provider for the given user and time range.
|
||||
*
|
||||
* Implementations should map provider-specific payloads to instances of
|
||||
* {@see RemoteEvent}.
|
||||
*
|
||||
* @return array|RemoteEvent[]
|
||||
*/
|
||||
public function listEventsForUser(User $user, \DateTimeImmutable $startDate, \DateTimeImmutable $endDate, ?int $offset = 0, ?int $limit = 50): array;
|
||||
|
||||
/**
|
||||
* Removes a calendar (single event) from the remote provider.
|
||||
*
|
||||
* **Note**: calendar (single event) which are canceled will appears in this
|
||||
* method, and not in syncCalendar method.
|
||||
*
|
||||
* Parameters:
|
||||
* - remoteId: the provider identifier of the remote event to delete. If
|
||||
* empty, implementations should no-op.
|
||||
* - remoteAttributes: provider-specific metadata previously stored with the
|
||||
* local entity (e.g. change keys, etags) that can help perform safe
|
||||
* concurrency checks when deleting. Implementations may ignore unknown
|
||||
* keys.
|
||||
* - user: the user in whose remote calendar the event lives and on whose
|
||||
* behalf the deletion must be performed.
|
||||
* - associatedCalendarRange: when provided, the implementation should
|
||||
* update/synchronize the corresponding remote busy-time block after the
|
||||
* event removal so that availability stays consistent.
|
||||
*/
|
||||
public function removeCalendar(string $remoteId, array $remoteAttributes, User $user, ?CalendarRange $associatedCalendarRange = null): void;
|
||||
|
||||
/**
|
||||
* Removes a remote busy-time block (calendar range) identified by
|
||||
* provider-specific id and attributes for the given user.
|
||||
*
|
||||
* Implementations should no-op if the id is empty.
|
||||
*/
|
||||
public function removeCalendarRange(string $remoteId, array $remoteAttributes, User $user): void;
|
||||
|
||||
/**
|
||||
* Synchronizes a Calendar entity to the remote provider.
|
||||
*
|
||||
* Typical cases to support (see MSGraph implementation):
|
||||
* - Creating the event on the remote calendar when it has no remote id.
|
||||
* - Updating the existing remote event when details or attendees change.
|
||||
* - Handling main user changes: cancel on the previous user's calendar,
|
||||
* (re)create associated ranges where needed, then create on the new
|
||||
* main user's calendar.
|
||||
* - If the Calendar uses a CalendarRange that already exists remotely,
|
||||
* implementations should remove/update that remote range when the event
|
||||
* becomes the source of truth for busy times.
|
||||
*
|
||||
* The implementation should not expects to receive calendar which are canceled
|
||||
* here.
|
||||
*
|
||||
* Parameters:
|
||||
* - calendar: the domain Calendar to mirror remotely.
|
||||
* - action: a hint about what triggered the sync; implementations should not rely
|
||||
* solely on this value and must base decisions on the Calendar state.
|
||||
* - previousCalendarRange: if the Calendar was previously attached to a
|
||||
* different range, this contains the former range so it can be recreated
|
||||
* remotely to preserve availability history when applicable.
|
||||
* - previousMainUser: the former main user, when the main user changed;
|
||||
* used to cancel the event in the previous user's calendar.
|
||||
* - oldInvites: the attendee snapshot before the change. Each item is an
|
||||
* array with keys: inviteId, userId, userEmail, userLabel.
|
||||
* - newInvites: the attendee snapshot after the change, same shape as
|
||||
* oldInvites. Implementations can compute diffs to add/remove attendees.
|
||||
*
|
||||
* The $action argument is a string tag indicating what happened to the
|
||||
* calendar. It MUST be one of the constants defined on
|
||||
* {@see CalendarMessage}:
|
||||
* - {@see CalendarMessage::CALENDAR_PERSIST}
|
||||
* - {@see CalendarMessage::CALENDAR_UPDATE}
|
||||
*
|
||||
* @param array<array{inviteId: int, userId: int, userEmail: int, userLabel: string}> $oldInvites
|
||||
*
|
||||
* @phpstan-param (CalendarMessage::CALENDAR_PERSIST|CalendarMessage::CALENDAR_UPDATE) $action
|
||||
*/
|
||||
public function syncCalendar(Calendar $calendar, string $action, ?CalendarRange $previousCalendarRange, ?User $previousMainUser, ?array $oldInvites, ?array $newInvites): void;
|
||||
|
||||
/**
|
||||
* Creates or updates a remote busy-time block representing the provided
|
||||
* CalendarRange. If the range has a remote id, it should be updated;
|
||||
* otherwise it should be created remotely, and the range enriched with
|
||||
* the new id/attributes by the caller.
|
||||
*/
|
||||
public function syncCalendarRange(CalendarRange $calendarRange): void;
|
||||
|
||||
/**
|
||||
* Synchronizes a single Invite (attendee) change to the remote provider.
|
||||
* Implementations may need to lookup the attendee's personal calendar to
|
||||
* find provider-specific identifiers before patching the main event.
|
||||
*/
|
||||
public function syncInvite(Invite $invite): void;
|
||||
}
|
||||
|
||||
@@ -35,25 +35,46 @@ use TheNetworg\OAuth2\Client\Provider\Azure;
|
||||
|
||||
class RemoteCalendarCompilerPass implements CompilerPassInterface
|
||||
{
|
||||
private const ZIMBRA_CONNECTOR = 'Chill\ZimbraBundle\Calendar\Connector\ZimbraConnector';
|
||||
|
||||
private const MS_GRAPH_SERVICES_TO_REMOVE = [
|
||||
MapAndSubscribeUserCalendarCommand::class,
|
||||
AzureGrantAdminConsentAndAcquireToken::class,
|
||||
RemoteCalendarConnectAzureController::class,
|
||||
MachineTokenStorage::class,
|
||||
MachineHttpClient::class,
|
||||
MSGraphRemoteCalendarConnector::class,
|
||||
MSUserAbsenceReaderInterface::class,
|
||||
MSUserAbsenceSync::class,
|
||||
];
|
||||
|
||||
public function process(ContainerBuilder $container)
|
||||
{
|
||||
$config = $container->getParameter('chill_calendar');
|
||||
$config = $container->getParameter('chill_calendar.remote_calendar_dsn');
|
||||
if (true === $container->getParameter('chill_calendar')['remote_calendars_sync']['microsoft_graph']['enabled']) {
|
||||
$dsn = 'msgraph://default';
|
||||
} else {
|
||||
$dsn = $config;
|
||||
}
|
||||
|
||||
if (true === $config['remote_calendars_sync']['microsoft_graph']['enabled']) {
|
||||
$scheme = parse_url($dsn, PHP_URL_SCHEME);
|
||||
|
||||
if ('msgraph' === $scheme) {
|
||||
$connector = MSGraphRemoteCalendarConnector::class;
|
||||
|
||||
$container->setAlias(HttpClientInterface::class.' $machineHttpClient', MachineHttpClient::class);
|
||||
} else {
|
||||
} elseif ('zimbra+http' === $scheme || 'zimbra+https' === $scheme) {
|
||||
$connector = self::ZIMBRA_CONNECTOR;
|
||||
foreach (self::MS_GRAPH_SERVICES_TO_REMOVE as $serviceId) {
|
||||
$container->removeDefinition($serviceId);
|
||||
}
|
||||
} elseif ('null' === $scheme) {
|
||||
$connector = NullRemoteCalendarConnector::class;
|
||||
// remove services which cannot be loaded
|
||||
$container->removeDefinition(MapAndSubscribeUserCalendarCommand::class);
|
||||
$container->removeDefinition(AzureGrantAdminConsentAndAcquireToken::class);
|
||||
$container->removeDefinition(RemoteCalendarConnectAzureController::class);
|
||||
$container->removeDefinition(MachineTokenStorage::class);
|
||||
$container->removeDefinition(MachineHttpClient::class);
|
||||
$container->removeDefinition(MSGraphRemoteCalendarConnector::class);
|
||||
$container->removeDefinition(MSUserAbsenceReaderInterface::class);
|
||||
$container->removeDefinition(MSUserAbsenceSync::class);
|
||||
foreach (self::MS_GRAPH_SERVICES_TO_REMOVE as $serviceId) {
|
||||
$container->removeDefinition($serviceId);
|
||||
}
|
||||
} else {
|
||||
throw new \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException('Unsupported remote calendar scheme: '.$scheme);
|
||||
}
|
||||
|
||||
if (!$container->hasAlias(Azure::class) && $container->hasDefinition('knpu.oauth2.client.azure')) {
|
||||
@@ -62,7 +83,9 @@ class RemoteCalendarCompilerPass implements CompilerPassInterface
|
||||
|
||||
foreach ([
|
||||
NullRemoteCalendarConnector::class,
|
||||
MSGraphRemoteCalendarConnector::class, ] as $serviceId) {
|
||||
MSGraphRemoteCalendarConnector::class,
|
||||
self::ZIMBRA_CONNECTOR,
|
||||
] as $serviceId) {
|
||||
if ($connector === $serviceId) {
|
||||
$container->getDefinition($serviceId)
|
||||
->setDecoratedService(RemoteCalendarConnectorInterface::class);
|
||||
|
||||
@@ -87,6 +87,8 @@ remote_ms_graph:
|
||||
|
||||
remote_calendar:
|
||||
calendar_range_title: Plage de disponibilité Chill
|
||||
# small type-hint in remote calendar to says that the appointment is created through an invitation, and not as main referrer
|
||||
calendar_invite_statement_in_calendar: Par invitation
|
||||
|
||||
invite:
|
||||
accepted: Accepté
|
||||
|
||||
@@ -26,6 +26,7 @@ class AddressRender implements ChillEntityRenderInterface
|
||||
'with_delimiter' => false,
|
||||
'has_no_address' => false,
|
||||
'multiline' => true,
|
||||
'separator' => ' — ',
|
||||
/* deprecated */
|
||||
'extended_infos' => false,
|
||||
];
|
||||
@@ -114,7 +115,9 @@ class AddressRender implements ChillEntityRenderInterface
|
||||
|
||||
public function renderString($addr, array $options): string
|
||||
{
|
||||
return implode(' — ', $this->renderLines($addr));
|
||||
$opts = [...self::DEFAULT_OPTIONS, ...$options];
|
||||
|
||||
return implode($opts['separator'], $this->renderLines($addr));
|
||||
}
|
||||
|
||||
public function supports($entity, array $options): bool
|
||||
|
||||
@@ -52,7 +52,7 @@ class CommentRender implements ChillEntityRenderInterface
|
||||
|
||||
public function renderString($entity, array $options): string
|
||||
{
|
||||
return $entity->getComment();
|
||||
return (string) $entity->getComment();
|
||||
}
|
||||
|
||||
public function supports($entity, array $options): bool
|
||||
|
||||
Reference in New Issue
Block a user