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