<?php
namespace App\Controller\Webhook;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Request;
use Google_Client;
use Google_Service_Calendar;
use App\Entity\SynchronisationSetting;
use App\Entity\GoogleAgenda;
use Symfony\Component\Mercure\PublisherInterface;
use Symfony\Component\Mercure\Update;
use App\Service\Synchronisation\SynchronisationLogger;
use Google\Service\Exception as GoogleServiceException;
class GoogleWebhookController extends AbstractController
{
public $logger;
private $publisher;
private SynchronisationLogger $syncLogger;
public function __construct(LoggerInterface $logger,PublisherInterface $publisher,SynchronisationLogger $syncLogger)
{
$this->logger = $logger;
$this->publisher = $publisher;
$this->syncLogger = $syncLogger;
}
/**
* @Route("/webhook/google-calendar", methods={"POST"})
*/
public function handleGoogleCalendarWebhook(Request $request)
{
// Verify the request is coming from Google (implement security measures).
// Extract channel information from request headers.
$resourceId = $request->headers->get('x-goog-resource-id');
$channelId = $request->headers->get('x-goog-channel-id');
$this->logger->info('New syhro', ['event status' => $request->headers->all() ]);
// Check if the request is valid.
if (!$resourceId || !$channelId) {
return new Response('Invalid request', Response::HTTP_BAD_REQUEST);
}
$entityManager = $this->getDoctrine()->getManager();
// Perform incremental synchronization to fetch changed events.
$changedEvents = $this->fetchChangedEvents($channelId, $resourceId);
// Handle the changed events based on your application's logic.
foreach ($changedEvents as $event) {
$synchronisation = $this->getDoctrine()->getRepository(SynchronisationSetting::class)
->findOneBy(array('channelGoogleNotif' => $channelId));
if($event->status == "cancelled")
{
$googleAgenda = $this->getDoctrine()->getRepository(GoogleAgenda::class)
->findOneBy(array('eventId' => $event->id));
if($googleAgenda){
// 1. Get data BEFORE deleting (to get the ID)
$publishedData = $this->formatAgendaData($googleAgenda, 'google_deleted');
$update = new Update('rdv-calendar-topic', json_encode($publishedData));
$this->logger->info('Mercure', ['action' => 'publish_deleted', 'eventId' => $event->id]);
$this->publisher->__invoke($update);
$entityManager->remove($googleAgenda);
$entityManager->flush();
}
}
else
{
//if(strpos($event->getDescription(), 'Nom du patient: ') === false){
$agendaGoogle = $this->getDoctrine()->getRepository(GoogleAgenda::class)
->findOneBy(array('eventId' => $event->id));
if(!$agendaGoogle)
{
$agendaGoogle = new GoogleAgenda();
}
$organizer = $event->getOrganizer();
$organizerEmail = $event->getOrganizer()->getEmail();
$agendaGoogle->setEventId($event->getId());
$agendaGoogle->setSummary($event->getSummary());
$startDateTime = new \DateTime($event->getStart()->getDateTime());
$agendaGoogle->setStart($startDateTime);
$endDateTime = new \DateTime($event->getEnd()->getDateTime());
$agendaGoogle->setEnd($endDateTime);
$meetingUrl = $event->getHangoutLink();
if (!empty($meetingUrl)) {
$agendaGoogle->setMeetingUrl($meetingUrl);
}
$attendees = $event->getAttendees();
if (!empty($attendees)) {
$attendeeEmails = array();
foreach ($attendees as $attendee) {
$attendeeEmails[] = $attendee->getEmail();
}
$serializedAttend = json_encode(['invited' => $attendeeEmails]);
$agendaGoogle->setAttendees($serializedAttend);
}
$attachments = $event->getAttachments();
if (!empty($attachments)) {
$attachementsUrl = array();
$attachementsName = array();
$attachementsIcon = array();
foreach ($attachments as $attachment) {
$attachementsUrl[] = $attachment->fileUrl;
$attachementsName[] = $attachment->title;
//$attachementsName[] = $attachment->mimeType;
$attachementsIcon[] = $attachment->iconLink;
}
$serializedattAchements = json_encode(['attachementUrl' => $attachementsUrl,"attachementName" => $attachementsName,"attachementsIcon" => $attachementsIcon]);
$agendaGoogle->setAttachement($serializedattAchements);
}
$agendaGoogle->setColor($event->getColorId());
$agendaGoogle->setLocation($event->getLocation());
$agendaGoogle->setDescription($event->getDescription());
$agendaGoogle->setAudio($synchronisation->getAudio());
$agendaGoogle->setSynchronisationSetting($synchronisation);
$agendaGoogle->setOrganiserEmail($organizerEmail);
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($agendaGoogle);
$entityManager->flush();
// Determine if it was a create or update action
$action = $agendaGoogle->getId() ? 'google_updated' : 'google_created';
// 1. Publish the update with the full event data
$publishedData = $this->formatAgendaData($agendaGoogle, $action);
$this->logger->info('Mercure', ['action' => 'publish_'.$action, 'eventId' => $event->id]);
$update = new Update('rdv-calendar-topic', json_encode($publishedData));
$this->publisher->__invoke($update);
//}
}
//if event is deleted $event->status = "cancelled"; we delete in our database $event->id;
// $this->logger->info('New syhro', ['event status' => $event->status ]);
}
// Return a response to acknowledge receipt of the webhook.
return new Response('', Response::HTTP_OK);
}
public function fetchChangedEvents($channelId, $resourceId)
{
try {
$client = new \Google_Client();
$em = $this->getDoctrine()->getManager();
$syncRepo = $em->getRepository(SynchronisationSetting::class);
$agendaRepo = $em->getRepository(GoogleAgenda::class);
// 1️⃣ Find the synchronisation by channel ID
$synchronisation = $syncRepo->findOneBy(['channelGoogleNotif' => $channelId]);
if (!$synchronisation) {
$this->syncLogger->log(
'GoogleWebhook',
"No synchronisation found for channelId: $channelId"
);
return [];
}
// 2️⃣ Initialize async token if null
if ($synchronisation->getAsynchToken() === null) {
$this->logger->info('New synchro', ['channelId' => $channelId]);
$googleAgendaList = $agendaRepo->findBy(['synchronisationSetting' => $synchronisation]);
$tokenSynch = null;
foreach ($googleAgendaList as $google) {
if ($google->getSynchToken() !== null) {
$tokenSynch = $google->getSynchToken();
break;
}
}
if ($tokenSynch !== null) {
$synchronisation->setAsynchToken($tokenSynch);
$em->persist($synchronisation);
$em->flush();
}
}
// 3️⃣ Prepare client and service
$accessToken = $synchronisation->getAccessToken();
$client->setAccessToken($accessToken);
$service = new \Google_Service_Calendar($client);
$syncToken = $synchronisation->getAsynchToken();
// 4️⃣ Call Google API safely
try {
$events = $service->events->listEvents('primary', [
'syncToken' => $syncToken,
]);
} catch (\Google\Service\Exception $e) {
$errorDetails = json_decode($e->getMessage(), true);
if (
$errorDetails
&& isset($errorDetails['error']['errors'][0]['reason'])
&& $errorDetails['error']['errors'][0]['reason'] === 'fullSyncRequired'
) {
$this->syncLogger->log(
'GoogleCalendarService',
'Full sync required — resync triggered',
$synchronisation,
$e->getMessage()
);
$events = $service->events->listEvents('primary');
} else {
// log and rethrow
$this->syncLogger->log(
'GoogleCalendarService',
'Google API error in fetchChangedEvents',
$synchronisation,
$e->getMessage()
);
throw $e;
}
}
// 5️⃣ Update sync token
$updatedSyncToken = $events->getNextSyncToken();
$synchronisation->setAsynchToken($updatedSyncToken);
$em->persist($synchronisation);
$em->flush();
$this->logger->info('New synchro', ['synchroToken' => $events->getItems()]);
return $events->getItems();
} catch (\Throwable $e) {
// 6️⃣ Catch any unexpected error (DB issue, bad token, etc.)
$this->syncLogger->log(
'GoogleWebhook',
'Unexpected error during fetchChangedEvents',
null,
$e->getMessage()
);
return [];
}
}
public function fetchNewlyCreatedEvents($channelId, $resourceId)
{
// Initialize the Google Client and set the user's OAuth token.
$client = new Google_Client();
$accessToken = "ya29.a0AfB_byCszSh4hIkDJDOMQQ6sLgq3X2HTS7_zIwuv_7Eur2D8x3fsQqdOGukDUyxrmYYWacK_U9ZDjflymb3aEWWZHYlYn2JQ2DZbgQwZvwPIY_RlNP-EK0vpPq4nA0EU_87pzrElgV4q6J2nsdDKKZNMAzWJRPXTJwaCgYKATYSARESFQGOcNnCzMT5hVsmwi90IUr1KWL4gA0169";
$client->setAccessToken($accessToken);
// Authenticate and set up the Calendar service for the user.
$service = new Google_Service_Calendar($client);
// Fetch the specific event associated with the provided $resourceId.
$event = $service->events->get('primary', $resourceId);
// Check if the event's resource state indicates it's a new event.
if ($event->getHtmlLink() && $event->getEtag() && $event->getUpdated()) {
// This is a newly created event.
$this->logger->info('New event created', ['event' => $event->getId()]);
return $event;
} else {
// The event is not new.
$this->logger->info('Event is not new', ['event' => $event->getId()]);
return null;
}
}
// Inside App\Controller\Webhook\GoogleWebhookController
private function formatAgendaData(GoogleAgenda $agenda, string $action): array
{
// The event ID from Google Calendar
$id = $agenda->getEventId();
// Deserialize attendee/attachment data if needed
$invited = $agenda->getAttendees() ? json_decode($agenda->getAttendees(), true) : [];
$attachement = $agenda->getAttachement() ? json_decode($agenda->getAttachement(), true) : null;
$data = [
"action" => $action, // <-- IMPORTANT: The specific action
"id" => $id,
"idEvent" => $id,
"eventId" => $id, // Use the Google Event ID for unique identification
"centerName" => $agenda->getSummary(),
"start" => $agenda->getStart() ? $agenda->getStart()->format(\DateTimeInterface::ATOM) : null,
"end" => $agenda->getEnd() ? $agenda->getEnd()->format(\DateTimeInterface::ATOM) : null,
"startDate" => $agenda->getStart() ? $agenda->getStart()->format('Y-m-d H:i') : null,
"endDate" => $agenda->getEnd() ? $agenda->getEnd()->format('Y-m-d H:i') : null,
"meet" => $agenda->getMeetingUrl(),
"invited" => $invited['invited'] ?? [],
"attachement" => $attachement,
"location" => $agenda->getLocation(),
"organiser" => $agenda->getOrganiserEmail(),
"backgroundColor" => '#F9D04F', // Yellow Color for Google Events
"borderColor" => '#F9D04F', // Yellow Color
"comment" => $agenda->getDescription(),
"editable" => false,
"durationEditable" => true,
"resourceEditable" => true,
"cache" => false,
"google" => true,
"event_title" => "<b>" . $agenda->getSummary() . "</b> ",
];
// Ensure the event data ID is unique for FullCalendar
$data['id'] = "google_" . $id;
return $data;
}
}