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