3 namespace Rapsys\AirBundle\Command
;
5 use Doctrine\Persistence\ManagerRegistry
;
6 use Symfony\Component\Cache\Adapter\FilesystemAdapter
;
7 use Symfony\Component\Console\Command\Command
;
8 use Symfony\Component\Console\Input\InputInterface
;
9 use Symfony\Component\Console\Output\OutputInterface
;
10 use Symfony\Component\DependencyInjection\ContainerInterface
;
11 use Symfony\Component\Routing\Generator\UrlGeneratorInterface
;
12 use Symfony\Component\Routing\RouterInterface
;
13 use Symfony\Component\Translation\TranslatorInterface
;
14 use Twig\Extra\Markdown\DefaultMarkdown
;
16 use Rapsys\AirBundle\Entity\Session
;
18 class CalendarCommand
extends Command
{
19 //Set failure constant
22 ///Set success constant
31 * @var ManagerRegistry
38 ///Translator instance
39 protected $translator;
42 * Inject doctrine, container and translator interface
44 * @param ContainerInterface $container The container instance
45 * @param ManagerRegistry $doctrine The doctrine instance
46 * @param RouterInterface $router The router instance
47 * @param TranslatorInterface $translator The translator instance
49 public function __construct(ContainerInterface
$container, ManagerRegistry
$doctrine, RouterInterface
$router, TranslatorInterface
$translator) {
50 //Call parent constructor
51 parent
::__construct();
54 $this->config
= $container->getParameter($this->getAlias());
57 $this->locale
= $container->getParameter('kernel.default_locale');
60 $this->doctrine
= $doctrine;
63 $this->router
= $router;
66 $context = $this->router
->getContext();
69 $context->setHost('airlibre.eu');
72 $context->setScheme('https');
75 $this->translator
= $translator;
78 ///Configure attribute command
79 protected function configure() {
83 ->setName('rapsysair:calendar')
84 //Set description shown with bin/console list
85 ->setDescription('Synchronize sessions in calendar')
86 //Set description shown with bin/console --help airlibre:attribute
87 ->setHelp('This command synchronize sessions in google calendar');
90 ///Process the attribution
91 protected function execute(InputInterface
$input, OutputInterface
$output) {
93 $period = new \
DatePeriod(
94 //Start from last week
95 new \
DateTime('-1 week'),
97 new \
DateInterval('P1D'),
98 //End with next 2 week
99 new \
DateTime('+2 week')
102 //Retrieve events to update
103 $sessions = $this->doctrine
->getRepository(Session
::class)->fetchAllByDatePeriod($period, $this->locale
);
105 //Markdown converted instance
106 $markdown = new DefaultMarkdown
;
108 //Retrieve cache object
109 //XXX: by default stored in /tmp/symfony-cache/@/W/3/6SEhFfeIW4UMDlAII+Dg
110 //XXX: stored in %kernel.project_dir%/var/cache/airlibre/0/P/IA20X0K4dkMd9-+Ohp9Q
111 $cache = new FilesystemAdapter($this->config
['cache']['namespace'], $this->config
['cache']['lifetime'], $this->config
['path']['cache']);
114 $cacheCalendars = $cache->getItem('calendars');
117 if (!$cacheCalendars->isHit()) {
119 return self
::FAILURE
;
123 $calendars = $cacheCalendars->get();
125 //XXX: calendars content
126 #var_export($calendars);
128 # '635317121880-usqucmne71jnmprl8br9khh2om4n8cmh.apps.googleusercontent.com' => [
129 # 'project' => 'calendar-317315',
130 # 'secret' => 'HRsKd4FIc9gxQHM4IoBWnlbD',
131 # 'redirect' => 'https://airlibre.eu/calendar/callback',
133 # 'ya29.a0ARrdaM_cNpedJ-B3irC76_0-C7cfF-WmMh0smAs4m7cSvBChnniWr-e79q0IfAbh5DSG4FlHbCMvmaYb7xX4V45PujT2U4InZmpHfspiPv-QeR4XeZJp7bLXwnw7A4M0imeeYyQcwCW7GJ8O7dGLBQlBZAvt_Q' => [
134 # 'calendar' => 'airlibre',
136 # 'scope' => 'https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/calendar.events',
137 # 'type' => 'Bearer',
138 # 'created' => 1625417137,
144 //Check expired token
145 foreach($calendars as $clientId => $client) {
147 $googleClient = new \Google\
Client(['application_name' => $client['project'], 'client_id' => $clientId, 'client_secret' => $client['secret'], 'redirect_uri' => $client['redirect']]);
149 //Iterate on each tokens
150 foreach($client['tokens'] as $tokenId => $token) {
152 $googleClient->setAccessToken(
154 'access_token' => $tokenId,
155 'refresh_token' => $token['refresh'],
156 'expires_in' => $token['expire'],
157 'scope' => $token['scope'],
158 'token_type' => $token['type'],
159 'created' => $token['created']
164 if ($exp = $googleClient->isAccessTokenExpired()) {
166 if (($refreshToken = $googleClient->getRefreshToken()) && ($googleToken = $googleClient->fetchAccessTokenWithRefreshToken($refreshToken)) && empty($googleToken['error'])) {
167 //Add refreshed token
168 $calendars[$clientId]['tokens'][$googleToken['access_token']] = [
169 'calendar' => $token['calendar'],
170 'prefix' => $token['prefix'],
171 'refresh' => $googleToken['refresh_token'],
172 'expire' => $googleToken['expires_in'],
173 'scope' => $googleToken['scope'],
174 'type' => $googleToken['token_type'],
175 'created' => $googleToken['created']
179 unset($calendars[$clientId]['tokens'][$tokenId]);
182 unset($calendars[$clientId]['tokens'][$tokenId]);
185 if (empty($calendars[$clientId]['tokens'])) {
187 unset($calendars[$clientId]);
191 $cacheCalendars->set($calendars);
194 $cache->save($cacheCalendars);
196 //Drop token and report
197 echo 'Token '.$tokenId.' for calendar '.$token['calendar'].' has expired and is not refreshable'."\n";
200 //XXX: we want that mail and stop here
201 return self
::FAILURE
;
208 $cacheCalendars->set($calendars);
211 $cache->save($cacheCalendars);
213 //Iterate on each calendar client
214 foreach($calendars as $clientId => $client) {
216 $googleClient = new \Google\
Client(['application_name' => $client['project'], 'client_id' => $clientId, 'client_secret' => $client['secret'], 'redirect_uri' => $client['redirect']]);
218 //Iterate on each tokens
219 foreach($client['tokens'] as $tokenId => $token) {
221 $googleClient->setAccessToken(
223 'access_token' => $tokenId,
224 'refresh_token' => $token['refresh'],
225 'expires_in' => $token['expire'],
226 'scope' => $token['scope'],
227 'token_type' => $token['type'],
228 'created' => $token['created']
233 if ($exp = $googleClient->isAccessTokenExpired()) {
234 //Last chance to skip this run
238 //Get google calendar
239 $googleCalendar = new \Google\Service\
Calendar($googleClient);
243 $calendar = $googleCalendar->calendars
->get($token['calendar']);
245 } catch(\Google\Service\Exception
$e) {
247 //TODO: handle codes here https://developers.google.com/calendar/api/guides/errors
248 echo 'Exception '.$e->getCode().':'.$e->getMessage().' in '.$e->getFile().' +'.$e->getLine()."\n";
249 echo $e->getTraceAsString()."\n";
252 return self
::FAILURE
;
260 //XXX: show even deleted event to be able to update them
261 'showDeleted' => true,
262 //TODO: fetch events one day before and one day after to avoid triggering double insert duplicate key 409 errors :=) on google
263 'timeMin' => $period->getStartDate()->format(\DateTime
::ISO8601
),
264 'timeMax' => $period->getEndDate()->format(\DateTime
::ISO8601
)
265 /*, 'iCalUID' => 'airlibre/?????'*//*'orderBy' => 'startTime', */
268 //Retrieve event collection
269 $googleEvents = $googleCalendar->events
->listEvents($token['calendar'], $filters);
271 //Iterate until reached end
273 //Iterate on each event
274 foreach ($googleEvents->getItems() as $event) {
276 if (preg_match('/^'.$token['prefix'].'([0-9]+)$/', $id = $event->getId(), $matches)) {
277 $events[$matches[1]] = $event;
278 //XXX: 3rd party events with id not matching prefix are skipped
280 # echo 'Skipping '.$event->getId().':'.$event->getSummary()."\n";*/
285 $pageToken = $googleEvents->getNextPageToken();
289 //Replace collection with next one
290 $googleEvents = $service->events
->listEvents($token['calendar'], $filters+
['pageToken' => $pageToken]);
296 //Iterate on each session to sync
297 foreach($sessions as $sessionId => $session) {
298 //Init shared properties
299 //TODO: validate for constraints here ??? https://developers.google.com/calendar/api/guides/extended-properties
301 'gps' => $session['l_latitude'].','.$session['l_longitude']
306 'title' => $this->translator
->trans('Session %id% by %pseudonym%', ['%id%' => $sessionId, '%pseudonym%' => $session['au_pseudonym']]).' '.$this->translator
->trans('at '.$session['l_title']),
307 'url' => $this->router
->generate('rapsys_air_session_view', ['id' => $sessionId], UrlGeneratorInterface
::ABSOLUTE_URL
)
311 $description = 'Description :'."\n".strip_tags(preg_replace('!<a href="([^"]+)"(?: title="[^"]+")?'.'>([^<]+)</a>!', '\1', $markdown->convert(strip_tags($session['p_description']))));
312 $shared['description'] = $markdown->convert(strip_tags($session['p_description']));
314 //Add class when available
315 if (!empty($session['p_class'])) {
316 $shared['class'] = $session['p_class'];
317 $description .= "\n\n".'Classe :'."\n".$session['p_class'];
320 //Add contact when available
321 if (!empty($session['p_contact'])) {
322 $shared['contact'] = $session['p_contact'];
323 $description .= "\n\n".'Contact :'."\n".$session['p_contact'];
326 //Add donate when available
327 if (!empty($session['p_donate'])) {
328 $shared['donate'] = $session['p_donate'];
329 $description .= "\n\n".'Contribuer :'."\n".$session['p_donate'];
332 //Add link when available
333 if (!empty($session['p_link'])) {
334 $shared['link'] = $session['p_link'];
335 $description .= "\n\n".'Site :'."\n".$session['p_link'];
338 //Add profile when available
339 if (!empty($session['p_profile'])) {
340 $shared['profile'] = $session['p_profile'];
341 $description .= "\n\n".'Réseau social :'."\n".$session['p_profile'];
345 if (!empty($session['locked']) && $events[$sessionId]) {
347 if (!empty($event = $events[$sessionId])) {
350 $googleCalendar->events
->delete($token['calendar'], $event->getId());
352 } catch(\Google\Service\Exception
$e) {
354 //TODO: handle codes here https://developers.google.com/calendar/api/guides/errors
355 echo 'Exception '.$e->getCode().':'.$e->getMessage().' in '.$e->getFile().' +'.$e->getLine()."\n";
356 echo $e->getTraceAsString()."\n";
359 return self
::FAILURE
;
363 } elseif (empty($events[$sessionId])) {
365 $event = new \Google\Service\Calendar\
Event(
367 //TODO: replace 'airlibre' with $this->config['calendar']['prefix'] when possible with prefix validating [a-v0-9]{5,}
368 //XXX: see https://developers.google.com/calendar/api/v3/reference/events/insert#id
369 'id' => $token['prefix'].$sessionId,
370 'summary' => $session['au_pseudonym'].' '.$this->translator
->trans('at '.$session['l_short']),
371 #'description' => $markdown->convert(strip_tags($session['p_description'])),
372 'description' => $description,
373 'status' => empty($session['a_canceled'])?'confirmed':'cancelled',
374 'location' => implode(' ', [$session['l_address'], $session['l_zipcode'], $session['l_city']]),
376 'extendedProperties' => [
380 //TODO: attendees[] ?
382 'dateTime' => $session['start']->format(\DateTime
::ISO8601
)
385 'dateTime' => $session['stop']->format(\DateTime
::ISO8601
)
392 $googleCalendar->events
->insert($token['calendar'], $event);
394 } catch(\Google\Service\Exception
$e) {
396 //TODO: handle codes here https://developers.google.com/calendar/api/guides/errors
397 echo 'Exception '.$e->getCode().':'.$e->getMessage().' in '.$e->getFile().' +'.$e->getLine()."\n";
398 echo $e->getTraceAsString()."\n";
401 return self
::FAILURE
;
406 $event = $events[$sessionId];
409 if ($session['updated'] >= (new \
DateTime($event->getUpdated()))) {
411 $event->setSummary($session['au_pseudonym'].' '.$this->translator
->trans('at '.$session['l_short']));
414 $event->setDescription($description);
417 $event->setStatus(empty($session['a_canceled'])?'confirmed':'cancelled');
420 $event->setLocation(implode(' ', [$session['l_address'], $session['l_zipcode'], $session['l_city']]));
423 $eventSource = $event->getSource();
425 //Update source title
426 $eventSource->setTitle($source['title']);
429 $eventSource->setUrl($source['url']);
432 #$event->setSource($source);
434 //Get extended properties
435 $extendedProperties = $event->getExtendedProperties();
438 $extendedProperties->setShared($shared);
441 //TODO: attendees[] ?
444 $start = $event->getStart();
446 //Update start datetime
447 $start->setDateTime($session['start']->format(\DateTime
::ISO8601
));
450 $end = $event->getEnd();
452 //Update stop datetime
453 $end->setDateTime($session['stop']->format(\DateTime
::ISO8601
));
457 $updatedEvent = $googleCalendar->events
->update($token['calendar'], $event->getId(), $event);
459 } catch(\Google\Service\Exception
$e) {
461 //TODO: handle codes here https://developers.google.com/calendar/api/guides/errors
462 echo 'Exception '.$e->getCode().':'.$e->getMessage().' in '.$e->getFile().' +'.$e->getLine()."\n";
463 echo $e->getTraceAsString()."\n";
466 return self
::FAILURE
;
470 //Drop from events array
471 unset($events[$sessionId]);
475 //Remaining events to drop
476 foreach($events as $eventId => $event) {
477 //Non canceled events
478 if ($event->getStatus() == 'confirmed') {
481 $googleCalendar->events
->delete($token['calendar'], $event->getId());
483 } catch(\Google\Service\Exception
$e) {
485 //TODO: handle codes here https://developers.google.com/calendar/api/guides/errors
486 echo 'Exception '.$e->getCode().':'.$e->getMessage().' in '.$e->getFile().' +'.$e->getLine()."\n";
487 echo $e->getTraceAsString()."\n";
490 return self
::FAILURE
;
498 return self
::SUCCESS
;
502 * Return the bundle alias
506 public function getAlias(): string {