]> Raphaël G. Git Repositories - airbundle/blob - Command/CalendarCommand.php
Add note about validating app
[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['path']['cache']);
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 //Check expired token
126 foreach($calendars as $clientId => $client) {
127 //Get google client
128 $googleClient = new \Google\Client(['application_name' => $client['project'], 'client_id' => $clientId, 'client_secret' => $client['secret'], 'redirect_uri' => $client['redirect']]);
129
130 //Iterate on each tokens
131 foreach($client['tokens'] as $tokenId => $token) {
132 //Set token
133 $googleClient->setAccessToken(
134 [
135 'access_token' => $tokenId,
136 'refresh_token' => $token['refresh'],
137 'expires_in' => $token['expire'],
138 'scope' => $token['scope'],
139 'token_type' => $token['type'],
140 'created' => $token['created']
141 ]
142 );
143
144 //With expired token
145 if ($exp = $googleClient->isAccessTokenExpired()) {
146 //Refresh token
147 if (($refreshToken = $googleClient->getRefreshToken()) && ($googleToken = $googleClient->fetchAccessTokenWithRefreshToken($refreshToken)) && empty($googleToken['error'])) {
148 //Add refreshed token
149 $calendars[$clientId]['tokens'][$googleToken['access_token']] = [
150 'calendar' => $token['calendar'],
151 'prefix' => $token['prefix'],
152 'refresh' => $googleToken['refresh_token'],
153 'expire' => $googleToken['expires_in'],
154 'scope' => $googleToken['scope'],
155 'type' => $googleToken['token_type'],
156 'created' => $googleToken['created']
157 ];
158
159 //Remove old token
160 unset($calendars[$clientId]['tokens'][$tokenId]);
161 } else {
162 //Drop token
163 unset($calendars[$clientId]['tokens'][$tokenId]);
164
165 //Without tokens
166 if (empty($calendars[$clientId]['tokens'])) {
167 //Drop client
168 unset($calendars[$clientId]);
169 }
170
171 //Save calendars
172 $cacheCalendars->set($calendars);
173
174 //Save calendar
175 $cache->save($cacheCalendars);
176
177 //Drop token and report
178 //XXX: submit app to avoid expiration
179 //XXX: see https://console.cloud.google.com/apis/credentials/consent?project=calendar-317315
180 echo 'Token '.$tokenId.' for calendar '.$token['calendar'].' has expired and is not refreshable'."\n";
181
182 //Return failure
183 //XXX: we want that mail and stop here
184 return self::FAILURE;
185 }
186 }
187 }
188 }
189
190 //Save calendars
191 $cacheCalendars->set($calendars);
192
193 //Save calendar
194 $cache->save($cacheCalendars);
195
196 //Iterate on each calendar client
197 foreach($calendars as $clientId => $client) {
198 //Get google client
199 $googleClient = new \Google\Client(['application_name' => $client['project'], 'client_id' => $clientId, 'client_secret' => $client['secret'], 'redirect_uri' => $client['redirect']]);
200
201 //Iterate on each tokens
202 foreach($client['tokens'] as $tokenId => $token) {
203 //Set token
204 $googleClient->setAccessToken(
205 [
206 'access_token' => $tokenId,
207 'refresh_token' => $token['refresh'],
208 'expires_in' => $token['expire'],
209 'scope' => $token['scope'],
210 'token_type' => $token['type'],
211 'created' => $token['created']
212 ]
213 );
214
215 //With expired token
216 if ($exp = $googleClient->isAccessTokenExpired()) {
217 //Last chance to skip this run
218 continue;
219 }
220
221 //Get google calendar
222 $googleCalendar = new \Google\Service\Calendar($googleClient);
223
224 //Retrieve calendar
225 try {
226 $calendar = $googleCalendar->calendars->get($token['calendar']);
227 //Catch exception
228 } catch(\Google\Service\Exception $e) {
229 //Display exception
230 //TODO: handle codes here https://developers.google.com/calendar/api/guides/errors
231 echo 'Exception '.$e->getCode().':'.$e->getMessage().' in '.$e->getFile().' +'.$e->getLine()."\n";
232 echo $e->getTraceAsString()."\n";
233
234 //Return failure
235 return self::FAILURE;
236 }
237
238 //Init events
239 $events = [];
240
241 //Set filters
242 $filters = [
243 //XXX: show even deleted event to be able to update them
244 'showDeleted' => true,
245 //TODO: fetch events one day before and one day after to avoid triggering double insert duplicate key 409 errors :=) on google
246 'timeMin' => $period->getStartDate()->format(\DateTime::ISO8601),
247 'timeMax' => $period->getEndDate()->format(\DateTime::ISO8601)
248 /*, 'iCalUID' => 'airlibre/?????'*//*'orderBy' => 'startTime', */
249 ];
250
251 //Retrieve event collection
252 $googleEvents = $googleCalendar->events->listEvents($token['calendar'], $filters);
253
254 //Iterate until reached end
255 while (true) {
256 //Iterate on each event
257 foreach ($googleEvents->getItems() as $event) {
258 //Store event by id
259 if (preg_match('/^'.$token['prefix'].'([0-9]+)$/', $id = $event->getId(), $matches)) {
260 $events[$matches[1]] = $event;
261 //XXX: 3rd party events with id not matching prefix are skipped
262 #} else {
263 # echo 'Skipping '.$event->getId().':'.$event->getSummary()."\n";*/
264 }
265 }
266
267 //Get page token
268 $pageToken = $googleEvents->getNextPageToken();
269
270 //Handle next page
271 if ($pageToken) {
272 //Replace collection with next one
273 $googleEvents = $service->events->listEvents($token['calendar'], $filters+['pageToken' => $pageToken]);
274 } else {
275 break;
276 }
277 }
278
279 //Iterate on each session to sync
280 foreach($sessions as $sessionId => $session) {
281 //Init shared properties
282 //TODO: validate for constraints here ??? https://developers.google.com/calendar/api/guides/extended-properties
283 //TODO: drop shared as unused ???
284 $shared = [
285 'gps' => $session['l_latitude'].','.$session['l_longitude']
286 ];
287
288 //Init source
289 $source = [
290 'title' => $this->translator->trans('Session %id% by %pseudonym%', ['%id%' => $sessionId, '%pseudonym%' => $session['au_pseudonym']]).' '.$this->translator->trans('at '.$session['l_title']),
291 'url' => $this->router->generate('rapsys_air_session_view', ['id' => $sessionId], UrlGeneratorInterface::ABSOLUTE_URL)
292 ];
293
294 //Init description
295 $description = 'Description :'."\n".strip_tags(preg_replace('!<a href="([^"]+)"(?: title="[^"]+")?'.'>([^<]+)</a>!', '\1', $markdown->convert(strip_tags($session['p_description']))));
296 $shared['description'] = $markdown->convert(strip_tags($session['p_description']));
297
298 //Add class when available
299 if (!empty($session['p_class'])) {
300 $shared['class'] = $session['p_class'];
301 $description .= "\n\n".'Classe :'."\n".$session['p_class'];
302 }
303
304 //Add contact when available
305 if (!empty($session['p_contact'])) {
306 $shared['contact'] = $session['p_contact'];
307 $description .= "\n\n".'Contact :'."\n".$session['p_contact'];
308 }
309
310 //Add donate when available
311 if (!empty($session['p_donate'])) {
312 $shared['donate'] = $session['p_donate'];
313 $description .= "\n\n".'Contribuer :'."\n".$session['p_donate'];
314 }
315
316 //Add link when available
317 if (!empty($session['p_link'])) {
318 $shared['link'] = $session['p_link'];
319 $description .= "\n\n".'Site :'."\n".$session['p_link'];
320 }
321
322 //Add profile when available
323 if (!empty($session['p_profile'])) {
324 $shared['profile'] = $session['p_profile'];
325 $description .= "\n\n".'Réseau social :'."\n".$session['p_profile'];
326 }
327
328 //Locked session
329 if (!empty($session['locked']) && $events[$sessionId]) {
330 //With events
331 if (!empty($event = $events[$sessionId])) {
332 try {
333 //Delete the event
334 $googleCalendar->events->delete($token['calendar'], $event->getId());
335 //Catch exception
336 } catch(\Google\Service\Exception $e) {
337 //Display exception
338 //TODO: handle codes here https://developers.google.com/calendar/api/guides/errors
339 echo 'Exception '.$e->getCode().':'.$e->getMessage().' in '.$e->getFile().' +'.$e->getLine()."\n";
340 echo $e->getTraceAsString()."\n";
341
342 //Return failure
343 return self::FAILURE;
344 }
345 }
346 //Without event
347 } elseif (empty($events[$sessionId])) {
348 //Init event
349 $event = new \Google\Service\Calendar\Event(
350 [
351 //TODO: replace 'airlibre' with $this->config['calendar']['prefix'] when possible with prefix validating [a-v0-9]{5,}
352 //XXX: see https://developers.google.com/calendar/api/v3/reference/events/insert#id
353 'id' => $token['prefix'].$sessionId,
354 'summary' => $session['au_pseudonym'].' '.$this->translator->trans('at '.$session['l_short']),
355 #'description' => $markdown->convert(strip_tags($session['p_description'])),
356 'description' => $description,
357 'status' => empty($session['a_canceled'])?'confirmed':'cancelled',
358 'location' => implode(' ', [$session['l_address'], $session['l_zipcode'], $session['l_city']]),
359 'source' => $source,
360 'extendedProperties' => [
361 'shared' => $shared
362 ],
363 //TODO: colorId ?
364 //TODO: attendees[] ?
365 'start' => [
366 'dateTime' => $session['start']->format(\DateTime::ISO8601)
367 ],
368 'end' => [
369 'dateTime' => $session['stop']->format(\DateTime::ISO8601)
370 ]
371 ]
372 );
373
374 try {
375 //Insert the event
376 $googleCalendar->events->insert($token['calendar'], $event);
377 //Catch exception
378 } catch(\Google\Service\Exception $e) {
379 //Display exception
380 //TODO: handle codes here https://developers.google.com/calendar/api/guides/errors
381 echo 'Exception '.$e->getCode().':'.$e->getMessage().' in '.$e->getFile().' +'.$e->getLine()."\n";
382 echo $e->getTraceAsString()."\n";
383
384 //Return failure
385 return self::FAILURE;
386 }
387 // With event
388 } else {
389 //Set event
390 $event = $events[$sessionId];
391
392 //With updated event
393 if ($session['updated'] >= (new \DateTime($event->getUpdated()))) {
394 //Set summary
395 $event->setSummary($session['au_pseudonym'].' '.$this->translator->trans('at '.$session['l_short']));
396
397 //Set description
398 $event->setDescription($description);
399
400 //Set status
401 $event->setStatus(empty($session['a_canceled'])?'confirmed':'cancelled');
402
403 //Set location
404 $event->setLocation(implode(' ', [$session['l_address'], $session['l_zipcode'], $session['l_city']]));
405
406 //Get source
407 $eventSource = $event->getSource();
408
409 //Update source title
410 $eventSource->setTitle($source['title']);
411
412 //Update source url
413 $eventSource->setUrl($source['url']);
414
415 //Set source
416 #$event->setSource($source);
417
418 //Get extended properties
419 $extendedProperties = $event->getExtendedProperties();
420
421 //Update shared
422 $extendedProperties->setShared($shared);
423
424 //TODO: colorId ?
425 //TODO: attendees[] ?
426
427 //Set start
428 $start = $event->getStart();
429
430 //Update start datetime
431 $start->setDateTime($session['start']->format(\DateTime::ISO8601));
432
433 //Set end
434 $end = $event->getEnd();
435
436 //Update stop datetime
437 $end->setDateTime($session['stop']->format(\DateTime::ISO8601));
438
439 try {
440 //Update the event
441 $updatedEvent = $googleCalendar->events->update($token['calendar'], $event->getId(), $event);
442 //Catch exception
443 } catch(\Google\Service\Exception $e) {
444 //Display exception
445 //TODO: handle codes here https://developers.google.com/calendar/api/guides/errors
446 echo 'Exception '.$e->getCode().':'.$e->getMessage().' in '.$e->getFile().' +'.$e->getLine()."\n";
447 echo $e->getTraceAsString()."\n";
448
449 //Return failure
450 return self::FAILURE;
451 }
452 }
453
454 //Drop from events array
455 unset($events[$sessionId]);
456 }
457 }
458
459 //Remaining events to drop
460 foreach($events as $eventId => $event) {
461 //Non canceled events
462 if ($event->getStatus() == 'confirmed') {
463 try {
464 //Delete the event
465 $googleCalendar->events->delete($token['calendar'], $event->getId());
466 //Catch exception
467 } catch(\Google\Service\Exception $e) {
468 //Display exception
469 //TODO: handle codes here https://developers.google.com/calendar/api/guides/errors
470 echo 'Exception '.$e->getCode().':'.$e->getMessage().' in '.$e->getFile().' +'.$e->getLine()."\n";
471 echo $e->getTraceAsString()."\n";
472
473 //Return failure
474 return self::FAILURE;
475 }
476 }
477 }
478 }
479 }
480
481 //Return success
482 return self::SUCCESS;
483 }
484
485 /**
486 * Return the bundle alias
487 *
488 * {@inheritdoc}
489 */
490 public function getAlias(): string {
491 return 'rapsys_air';
492 }
493 }