]> Raphaël G. Git Repositories - airbundle/blob - Command/CalendarCommand.php
Add calendar command
[airbundle] / Command / CalendarCommand.php
1 <?php
2
3 namespace Rapsys\AirBundle\Command;
4
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;
15
16 use Rapsys\AirBundle\Entity\Session;
17
18 class CalendarCommand extends Command {
19 //Set failure constant
20 const FAILURE = 1;
21
22 ///Set success constant
23 const SUCCESS = 0;
24
25 ///Config array
26 protected $config;
27
28 /**
29 * Doctrine instance
30 *
31 * @var ManagerRegistry
32 */
33 protected $doctrine;
34
35 ///Locale
36 protected $locale;
37
38 ///Translator instance
39 protected $translator;
40
41 /**
42 * Inject doctrine, container and translator interface
43 *
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
48 */
49 public function __construct(ContainerInterface $container, ManagerRegistry $doctrine, RouterInterface $router, TranslatorInterface $translator) {
50 //Call parent constructor
51 parent::__construct();
52
53 //Retrieve config
54 $this->config = $container->getParameter($this->getAlias());
55
56 //Retrieve locale
57 $this->locale = $container->getParameter('kernel.default_locale');
58
59 //Store doctrine
60 $this->doctrine = $doctrine;
61
62 //Store router
63 $this->router = $router;
64
65 //Get router context
66 $context = $this->router->getContext();
67
68 //Set host
69 $context->setHost('airlibre.eu');
70
71 //Set scheme
72 $context->setScheme('https');
73
74 //Set the translator
75 $this->translator = $translator;
76 }
77
78 ///Configure attribute command
79 protected function configure() {
80 //Configure the class
81 $this
82 //Set name
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');
88 }
89
90 ///Process the attribution
91 protected function execute(InputInterface $input, OutputInterface $output) {
92 //Compute period
93 $period = new \DatePeriod(
94 //Start from last week
95 new \DateTime('-1 week'),
96 //Iterate on each day
97 new \DateInterval('P1D'),
98 //End with next 2 week
99 new \DateTime('+2 week')
100 );
101
102 //Retrieve events to update
103 $sessions = $this->doctrine->getRepository(Session::class)->fetchAllByDatePeriod($period, $this->locale);
104
105 //Markdown converted instance
106 $markdown = new DefaultMarkdown;
107
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['cache']['directory']);
112
113 //Retrieve calendars
114 $cacheCalendars = $cache->getItem('calendars');
115
116 //Without calendars
117 if (!$cacheCalendars->isHit()) {
118 //Return failure
119 return self::FAILURE;
120 }
121
122 //Retrieve calendars
123 $calendars = $cacheCalendars->get();
124
125 //XXX: calendars content
126 #var_export($calendars);
127 #$debug = [
128 # '635317121880-usqucmne71jnmprl8br9khh2om4n8cmh.apps.googleusercontent.com' => [
129 # 'project' => 'calendar-317315',
130 # 'secret' => 'HRsKd4FIc9gxQHM4IoBWnlbD',
131 # 'redirect' => 'https://airlibre.eu/calendar/callback',
132 # 'tokens' => [
133 # 'ya29.a0ARrdaM_cNpedJ-B3irC76_0-C7cfF-WmMh0smAs4m7cSvBChnniWr-e79q0IfAbh5DSG4FlHbCMvmaYb7xX4V45PujT2U4InZmpHfspiPv-QeR4XeZJp7bLXwnw7A4M0imeeYyQcwCW7GJ8O7dGLBQlBZAvt_Q' => [
134 # 'calendar' => 'airlibre',
135 # 'expire' => 3599,
136 # 'scope' => 'https://www.googleapis.com/auth/calendar https://www.googleapis.com/auth/calendar.events',
137 # 'type' => 'Bearer',
138 # 'created' => 1625417137,
139 # ],
140 # ],
141 # ],
142 #];
143
144 //Check expired token
145 foreach($calendars as $clientId => $client) {
146 //Get google client
147 $googleClient = new \Google\Client(['application_name' => $client['project'], 'client_id' => $clientId, 'client_secret' => $client['secret'], 'redirect_uri' => $client['redirect']]);
148
149 //Iterate on each tokens
150 foreach($client['tokens'] as $tokenId => $token) {
151 //Set token
152 $googleClient->setAccessToken(
153 [
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']
160 ]
161 );
162
163 //With expired token
164 if ($exp = $googleClient->isAccessTokenExpired()) {
165 //Refresh token
166 if ($googleClient->getRefreshToken()) {
167 //Retrieve refreshed token
168 $googleToken = $googleClient->fetchAccessTokenWithRefreshToken($googleClient->getRefreshToken());
169
170 //Add refreshed token
171 $calendars[$clientId]['tokens'][$googleToken['access_token']] = [
172 'calendar' => $token['calendar'],
173 'prefix' => $token['prefix'],
174 'refresh' => $googleToken['refresh_token'],
175 'expire' => $googleToken['expires_in'],
176 'scope' => $googleToken['scope'],
177 'type' => $googleToken['token_type'],
178 'created' => $googleToken['created']
179 ];
180
181 //Remove old token
182 unset($calendars[$clientId]['tokens'][$tokenId]);
183 } else {
184 //Drop token
185 unset($calendars[$clientId]['tokens'][$tokenId]);
186
187 //Without tokens
188 if (empty($calendars[$clientId]['tokens'])) {
189 //Drop client
190 unset($calendars[$clientId]);
191 }
192
193 //Drop token and report
194 echo 'Token '.$tokenId.' for calendar '.$token['calendar'].' has expired and is not refreshable'."\n";
195
196 //Return failure
197 //XXX: we want that mail and stop here
198 return self::FAILURE;
199 }
200 }
201 }
202 }
203
204 //Save calendars
205 $cacheCalendars->set($calendars);
206
207 //Save calendar
208 $cache->save($cacheCalendars);
209
210 //Iterate on each calendar client
211 foreach($calendars as $clientId => $client) {
212 //Get google client
213 $googleClient = new \Google\Client(['application_name' => $client['project'], 'client_id' => $clientId, 'client_secret' => $client['secret'], 'redirect_uri' => $client['redirect']]);
214
215 //Iterate on each tokens
216 foreach($client['tokens'] as $tokenId => $token) {
217 //Set token
218 $googleClient->setAccessToken(
219 [
220 'access_token' => $tokenId,
221 'refresh_token' => $token['refresh'],
222 'expires_in' => $token['expire'],
223 'scope' => $token['scope'],
224 'token_type' => $token['type'],
225 'created' => $token['created']
226 ]
227 );
228
229 //With expired token
230 if ($exp = $googleClient->isAccessTokenExpired()) {
231 //Last chance to skip this run
232 continue;
233 }
234
235 //Get google calendar
236 $googleCalendar = new \Google\Service\Calendar($googleClient);
237
238 //Retrieve calendar
239 try {
240 $calendar = $googleCalendar->calendars->get($token['calendar']);
241 //Catch exception
242 } catch(\Google\Service\Exception $e) {
243 //Display exception
244 //TODO: handle codes here https://developers.google.com/calendar/api/guides/errors
245 echo 'Exception '.$e->getCode().':'.$e->getMessage().' in '.$e->getFile().' +'.$e->getLine()."\n";
246 echo $e->getTraceAsString()."\n";
247
248 //Return failure
249 return self::FAILURE;
250 }
251
252 //Init events
253 $events = [];
254
255 //Set filters
256 $filters = [
257 //XXX: show even deleted event to be able to update them
258 'showDeleted' => true,
259 //TODO: fetch events one day before and one day after to avoid triggering double insert duplicate key 409 errors :=) on google
260 'timeMin' => $period->getStartDate()->format(\DateTime::ISO8601),
261 'timeMax' => $period->getEndDate()->format(\DateTime::ISO8601)
262 /*, 'iCalUID' => 'airlibre/?????'*//*'orderBy' => 'startTime', */
263 ];
264
265 //Retrieve event collection
266 $googleEvents = $googleCalendar->events->listEvents($token['calendar'], $filters);
267
268 //Iterate until reached end
269 while (true) {
270 //Iterate on each event
271 foreach ($googleEvents->getItems() as $event) {
272 //Store event by id
273 if (preg_match('/^'.$token['prefix'].'([0-9]+)$/', $id = $event->getId(), $matches)) {
274 $events[$matches[1]] = $event;
275 //XXX: 3rd party events with id not matching prefix are skipped
276 #} else {
277 # echo 'Skipping '.$event->getId().':'.$event->getSummary()."\n";*/
278 }
279 }
280
281 //Get page token
282 $pageToken = $googleEvents->getNextPageToken();
283
284 //Handle next page
285 if ($pageToken) {
286 //Replace collection with next one
287 $googleEvents = $service->events->listEvents($token['calendar'], $filters+['pageToken' => $pageToken]);
288 } else {
289 break;
290 }
291 }
292
293 //Iterate on each session to sync
294 foreach($sessions as $sessionId => $session) {
295 //Init shared properties
296 //TODO: validate for constraints here ??? https://developers.google.com/calendar/api/guides/extended-properties
297 $shared = [
298 'gps' => $session['l_latitude'].','.$session['l_longitude']
299 ];
300
301 //Init source
302 $source = [
303 'title' => $this->translator->trans('Session %id% by %pseudonym%', ['%id%' => $sessionId, '%pseudonym%' => $session['au_pseudonym']]).' '.$this->translator->trans('at '.$session['l_title']),
304 'url' => $this->router->generate('rapsys_air_session_view', ['id' => $sessionId], UrlGeneratorInterface::ABSOLUTE_URL)
305 ];
306
307 //Init description
308 #$description = '<dl><dt>Description</dt><dd>'.$markdown->convert(strip_tags(str_replace(["\r", "\n\n"], ['', "\n"], $session['p_description']))).'</dd></dl>';
309 $description = '<dl><dt>Description</dt><dd>'.$markdown->convert(strip_tags($session['p_description'])).'</dd></dl>';
310
311 //Add class when available
312 if (!empty($session['p_class'])) {
313 $shared['class'] = $session['p_class'];
314 #$description .= '<dl><dt>Classe</dt><dd>'.$markdown->convert(strip_tags(str_replace(["\r", "\n\n"], ['', "\n"], $session['p_class']))).'</dd></dl>';
315 $description .= '<dl><dt>Classe</dt><dd><p>'.$session['p_class'].'</p></dd></dl>';
316 }
317
318 //Add contact when available
319 if (!empty($session['p_contact'])) {
320 $shared['contact'] = $session['p_contact'];
321 $description .= '<dl><dt>Contacter</dt><dd><p>'.$session['p_contact'].'</p></dd></dl>';
322 }
323
324 //Add donate when available
325 if (!empty($session['p_donate'])) {
326 $shared['donate'] = $session['p_donate'];
327 $description .= '<dl><dt>Contribuer</dt><dd><p>'.$session['p_donate'].'</p></dd></dl>';
328 }
329
330 //Add link when available
331 if (!empty($session['p_link'])) {
332 $shared['link'] = $session['p_link'];
333 $description .= '<dl><dt>Site</dt><dd><p>'.$session['p_link'].'</p></dd></dl>';
334 }
335
336 //Add profile when available
337 if (!empty($session['p_profile'])) {
338 $shared['profile'] = $session['p_profile'];
339 $description .= '<dl><dt>Réseau social</dt><dd><p>'.$session['p_profile'].'</p></dd></dl>';
340 }
341
342 //Locked session
343 if (!empty($session['locked']) && $events[$sessionId]) {
344 //With events
345 if (!empty($event = $events[$sessionId])) {
346 try {
347 //Delete the event
348 $googleCalendar->events->delete($token['calendar'], $event->getId());
349 //Catch exception
350 } catch(\Google\Service\Exception $e) {
351 //Display exception
352 //TODO: handle codes here https://developers.google.com/calendar/api/guides/errors
353 echo 'Exception '.$e->getCode().':'.$e->getMessage().' in '.$e->getFile().' +'.$e->getLine()."\n";
354 echo $e->getTraceAsString()."\n";
355
356 //Return failure
357 return self::FAILURE;
358 }
359 }
360 //Without event
361 } elseif (empty($events[$sessionId])) {
362 //Init event
363 $event = new \Google\Service\Calendar\Event(
364 [
365 //TODO: replace 'airlibre' with $this->config['calendar']['prefix'] when possible with prefix validating [a-v0-9]{5,}
366 //XXX: see https://developers.google.com/calendar/api/v3/reference/events/insert#id
367 'id' => $token['prefix'].$sessionId,
368 'summary' => $session['au_pseudonym'].' '.$this->translator->trans('at '.$session['l_short']),
369 #'description' => $markdown->convert(strip_tags($session['p_description'])),
370 'description' => $description,
371 'status' => empty($session['a_canceled'])?'confirmed':'cancelled',
372 'location' => implode(' ', [$session['l_address'], $session['l_zipcode'], $session['l_city']]),
373 'source' => $source,
374 'extendedProperties' => [
375 'shared' => $shared
376 ],
377 //TODO: colorId ?
378 //TODO: attendees[] ?
379 'start' => [
380 'dateTime' => $session['start']->format(\DateTime::ISO8601)
381 ],
382 'end' => [
383 'dateTime' => $session['stop']->format(\DateTime::ISO8601)
384 ]
385 ]
386 );
387
388 try {
389 //Insert the event
390 $googleCalendar->events->insert($token['calendar'], $event);
391 //Catch exception
392 } catch(\Google\Service\Exception $e) {
393 //Display exception
394 //TODO: handle codes here https://developers.google.com/calendar/api/guides/errors
395 echo 'Exception '.$e->getCode().':'.$e->getMessage().' in '.$e->getFile().' +'.$e->getLine()."\n";
396 echo $e->getTraceAsString()."\n";
397
398 //Return failure
399 return self::FAILURE;
400 }
401 // With event
402 } else {
403 //Set event
404 $event = $events[$sessionId];
405
406 //With updated event
407 #if ($session['updated'] >= (new \DateTime($event->getUpdated()))) {
408 {
409 //Set summary
410 $event->setSummary($session['au_pseudonym'].' '.$this->translator->trans('at '.$session['l_short']));
411
412 //Set description
413 $event->setDescription($description);
414
415 //Set status
416 $event->setStatus(empty($session['a_canceled'])?'confirmed':'cancelled');
417
418 //Set location
419 $event->setLocation(implode(' ', [$session['l_address'], $session['l_zipcode'], $session['l_city']]));
420
421 //Get source
422 $eventSource = $event->getSource();
423
424 //Update source title
425 $eventSource->setTitle($source['title']);
426
427 //Update source url
428 $eventSource->setUrl($source['url']);
429
430 //Set source
431 #$event->setSource($source);
432
433 //Get extended properties
434 $extendedProperties = $event->getExtendedProperties();
435
436 //Update shared
437 $extendedProperties->setShared($shared);
438
439 //TODO: colorId ?
440 //TODO: attendees[] ?
441
442 //Set start
443 $start = $event->getStart();
444
445 //Update start datetime
446 $start->setDateTime($session['start']->format(\DateTime::ISO8601));
447
448 //Set end
449 $end = $event->getEnd();
450
451 //Update stop datetime
452 $end->setDateTime($session['stop']->format(\DateTime::ISO8601));
453
454 try {
455 //Insert the event
456 $updatedEvent = $googleCalendar->events->update($token['calendar'], $event->getId(), $event);
457 //Catch exception
458 } catch(\Google\Service\Exception $e) {
459 //Display exception
460 //TODO: handle codes here https://developers.google.com/calendar/api/guides/errors
461 echo 'Exception '.$e->getCode().':'.$e->getMessage().' in '.$e->getFile().' +'.$e->getLine()."\n";
462 echo $e->getTraceAsString()."\n";
463
464 //Return failure
465 return self::FAILURE;
466 }
467 }
468
469 //Drop from events array
470 unset($events[$sessionId]);
471 }
472 }
473
474 //Remaining events to drop
475 foreach($events as $eventId => $event) {
476 //Non canceled events
477 if ($event->getStatus() == 'confirmed') {
478 try {
479 //Delete the event
480 $googleCalendar->events->delete($token['calendar'], $event->getId());
481 //Catch exception
482 } catch(\Google\Service\Exception $e) {
483 //Display exception
484 //TODO: handle codes here https://developers.google.com/calendar/api/guides/errors
485 echo 'Exception '.$e->getCode().':'.$e->getMessage().' in '.$e->getFile().' +'.$e->getLine()."\n";
486 echo $e->getTraceAsString()."\n";
487
488 //Return failure
489 return self::FAILURE;
490 }
491 }
492 }
493 }
494 }
495
496 //Return success
497 return self::SUCCESS;
498 }
499
500 /**
501 * Return the bundle alias
502 *
503 * {@inheritdoc}
504 */
505 public function getAlias(): string {
506 return 'rapsys_air';
507 }
508 }