X-Git-Url: https://git.rapsys.eu/airbundle/blobdiff_plain/ed0a31d7a909f78a031e955c87ae55323b6f7ba8..5ec45cd7fa8b353f4f211c0abfcc0c875a938c7e:/Command/Calendar2Command.php diff --git a/Command/Calendar2Command.php b/Command/Calendar2Command.php deleted file mode 100644 index 52eafb9..0000000 --- a/Command/Calendar2Command.php +++ /dev/null @@ -1,613 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Rapsys\AirBundle\Command; - -use Doctrine\Persistence\ManagerRegistry; - -use Google\Client; -use Google\Service\Calendar; -use Google\Service\Calendar\Event; -use Google\Service\Calendar\EventExtendedProperties; -use Google\Service\Calendar\EventSource; - -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Routing\Generator\UrlGeneratorInterface; -use Symfony\Component\Routing\RouterInterface; -use Symfony\Contracts\Cache\CacheInterface; -use Symfony\Contracts\Cache\ItemInterface; -use Symfony\Contracts\Translation\TranslatorInterface; - -use Twig\Extra\Markdown\DefaultMarkdown; - -use Rapsys\AirBundle\Command; -use Rapsys\AirBundle\Entity\GoogleCalendar; -use Rapsys\AirBundle\Entity\GoogleToken; -use Rapsys\AirBundle\Entity\Session; - -use Rapsys\PackBundle\Util\SluggerUtil; - -/** - * {@inheritdoc} - * - * Synchronize sessions in users' calendar - */ -class Calendar2Command extends Command { - /** - * Set description - * - * Shown with bin/console list - */ - protected string $description = 'Synchronize sessions in users\' calendar'; - - /** - * Set help - * - * Shown with bin/console --help rapsysair:calendar2 - */ - protected string $help = 'This command synchronize sessions in users\' google calendar'; - - /** - * Set domain - */ - protected string $domain; - - /** - * Set item - * - * Cache item instance - */ - protected ItemInterface $item; - - /** - * Set prefix - */ - protected string $prefix; - - /** - * Set service - * - * Google calendar instance - */ - protected Calendar $service; - - /** - * {@inheritdoc} - * - * @param CacheInterface $cache The cache instance - * @param Client $google The google client instance - * @param DefaultMarkdown $markdown The markdown instance - */ - public function __construct(protected ManagerRegistry $doctrine, protected string $locale, protected RouterInterface $router, protected SluggerUtil $slugger, protected TranslatorInterface $translator, protected CacheInterface $cache, protected Client $google, protected DefaultMarkdown $markdown) { - //Call parent constructor - parent::__construct($this->doctrine, $this->locale, $this->router, $this->slugger, $this->translator); - - //Replace google client redirect uri - $this->google->setRedirectUri($this->router->generate($this->google->getRedirectUri(), [], UrlGeneratorInterface::ABSOLUTE_URL)); - } - - /** - * Process the attribution - */ - protected function execute(InputInterface $input, OutputInterface $output): int { - //Get domain - $this->domain = $this->router->getContext()->getHost(); - - //Get manager - $manager = $this->doctrine->getManager(); - - //Convert from any to latin, then to ascii and lowercase - $trans = \Transliterator::create('Any-Latin; Latin-ASCII; Lower()'); - - //Replace every non alphanumeric character by dash then trim dash - $this->prefix = preg_replace(['/(\.[a-z0-9]+)+$/', '/[^a-v0-9]+/'], '', $trans->transliterate($this->domain)); - - //With too short prefix - if ($this->prefix === null || strlen($this->prefix) < 4) { - //Throw domain exception - throw new \DomainException('Prefix too short: '.$this->prefix); - } - - //Iterate on google tokens - foreach($tokens = $this->doctrine->getRepository(GoogleToken::class)->findAllIndexed() as $tid => $token) { - //Clear client cache before changing access token - //TODO: set a per token cache ? - $this->google->getCache()->clear(); - - //Set access token - $this->google->setAccessToken( - [ - 'access_token' => $token['access'], - 'refresh_token' => $token['refresh'], - 'created' => $token['created']->getTimestamp(), - 'expires_in' => $token['expired']->getTimestamp() - (new \DateTime('now'))->getTimestamp() - ] - ); - - //With expired token - if ($this->google->isAccessTokenExpired()) { - //Refresh token - if (($gRefresh = $this->google->getRefreshToken()) && ($gToken = $this->google->fetchAccessTokenWithRefreshToken($gRefresh)) && empty($gToken['error'])) { - //Get google token - $googleToken = $this->doctrine->getRepository(GoogleToken::class)->findOneById($token['id']); - - //Set access token - $googleToken->setAccess($gToken['access_token']); - - //Set expires - $googleToken->setExpired(new \DateTime('+'.$gToken['expires_in'].' second')); - - //Set refresh - $googleToken->setRefresh($gToken['refresh_token']); - - //Queue google token save - $manager->persist($googleToken); - - //Flush to get the ids - $manager->flush(); - //Refresh failed - } else { - //Show error - fprintf(STDERR, 'Unable to refresh token %d: %s', $token['id'], $gToken['error']?:''); - - //TODO: warn user by mail ? - - //Skip to next token - continue; - } - } - - //Get google calendar service - $this->service = new Calendar($this->google); - - //Iterate on google calendars - foreach($calendars = $token['calendars'] as $cid => $calendar) { - //Set start - $synchronized = null; - - //Set cache key - $cacheKey = 'command.calendar2.'.$this->slugger->short($calendar['mail']); - - //XXX: TODO: remove DEBUG - #$this->cache->delete($cacheKey); - - //Retrieve calendar events - try { - //Get events - $events = $this->cache->get( - //Cache key - //XXX: set to command.calendar2.$mail - $cacheKey, - //Fetch mail calendar event list - function (ItemInterface $item) use ($calendar, &$synchronized): array { - //Expire after 1h - $item->expiresAfter(3600); - - //Set synchronized - $synchronized = new \DateTime('now'); - - //Init events - $events = []; - - //Set filters - //TODO: add a filter to only retrieve - $filters = [ - //XXX: every event even deleted one to be able to update them - 'showDeleted' => true, - //XXX: every instances - 'singleEvents' => false, - //XXX: select only domain events - 'privateExtendedProperty' => 'domain='.$this->domain - #TODO: restrict events even more by time or updated datetime ? (-1 week to +2 week) - //TODO: fetch events one day before and one day after to avoid triggering double insert duplicate key 409 errors :=) on google - #'timeMin' => $period->getStartDate()->format(\DateTime::ISO8601), - #'timeMax' => $period->getEndDate()->format(\DateTime::ISO8601) - /*, 'iCalUID' => 'airlibre/?????'*//*'orderBy' => 'startTime', */ - //updatedMin => new \DateTime('-1 week') ? - ]; - - //Set page token - $pageToken = null; - - //Iterate until next page token is null - do { - //Get calendar events list - //XXX: see vendor/google/apiclient-services/src/Calendar/Resource/Events.php +289 - $eventList = $this->service->events->listEvents($calendar['mail'], ['pageToken' => $pageToken]+$filters); - - //Iterate on items - foreach($eventList->getItems() as $event) { - //With extended properties - if (($properties = $event->getExtendedProperties()) && ($private = $properties->getPrivate()) && isset($private['id']) && ($id = $private['id']) && isset($private['domain']) && $private['domain'] == $this->domain) { - //Add event - $events[$id] = $event; - //XXX: 3rd party events without matching prefix and id are skipped - #} else { - # #echo 'Skipping '.$event->getId().':'.$event->getSummary()."\n"; - # echo 'Skipping '.$id.':'.$event->getSummary()."\n"; - } - } - } while ($pageToken = $eventList->getNextPageToken()); - - //Return events - return $events; - } - ); - //Catch exception - } catch(\Google\Service\Exception $e) { - //With 401 or code - //XXX: see https://cloud.google.com/apis/design/errors - if ($e->getCode() == 401 || $e->getCode() == 403) { - //Show error - fprintf(STDERR, 'Unable to list calendar %d events: %s', $calendar['id'], $e->getMessage()?:''); - - //TODO: warn user by mail ? - - //Skip to next token - continue; - } - - //Throw error - throw new \LogicException('Calendar event list failed', 0, $e); - } - - //Store cache item - $this->item = $this->cache->getItem($cacheKey); - - //Iterate on sessions to update - foreach($this->doctrine->getRepository(Session::class)->findAllByUserIdSynchronized($token['uid'], $calendar['synchronized']) as $session) { - //Start exception catching - try { - //Without event - if (!isset($events[$session['id']])) { - //Insert event - $this->insert($calendar['mail'], $session); - //With locked session - } elseif (($event = $events[$session['id']]) && !empty($session['locked'])) { - //Delete event - $sid = $this->delete($calendar['mail'], $event); - - //Drop from events array - unset($events[$sid]); - //With event to update - } elseif ($session['modified'] > (new \DateTime($event->getUpdated()))) { - //Update event - $sid = $this->update($calendar['mail'], $event, $session); - - //Drop from events array - unset($events[$sid]); - } - //Catch exception - } catch(\Google\Service\Exception $e) { - //Identifier already exists - if ($e->getCode() == 409) { - //Get calendar event - //XXX: see vendor/google/apiclient-services/src/Calendar/Resource/Events.php +81 - $event = $this->service->events->get($calendar['mail'], $this->prefix.$session['id']); - - //Update required - if ($session['modified'] > (new \DateTime($event->getUpdated()))) { - //Update event - $sid = $this->update($calendar['mail'], $event, $session); - - //Drop from events array - unset($events[$sid]); - } - //TODO: handle other codes gracefully ? (503 & co) - //Other errors - } else { - //Throw error - throw new \LogicException(sprintf('Calendar %s event %s operation failed', $calendar, $this->prefix.$session['id']), 0, $e); - } - } - } - - //Get all sessions - $sessions = $this->doctrine->getRepository(Session::class)->findAllByUserIdSynchronized($token['uid']); - - //Remaining events to drop - foreach($events as $eid => $event) { - //With events updated since last synchronized - if ($event->getStatus() == 'confirmed' && (new \DateTime($event->getUpdated())) > $calendar['synchronized']) { - //TODO: Add a try/catch here to handle error codes gracefully (503 & co) ? - //With event to update - if (isset($sessions[$eid]) && ($session = $sessions[$eid]) && empty($session['locked'])) { - //Update event - $sid = $this->update($calendar['mail'], $event, $session); - - //Drop from events array - unset($events[$sid]); - //With locked or unknown session - } else { - //Delete event - $sid = $this->delete($calendar['mail'], $event); - - //Drop from events array - unset($events[$sid]); - } - } - } - - //Persist cache item - $this->cache->commit(); - - //With synchronized - //XXX: only store synchronized on run without caching - if ($synchronized) { - //Get google calendar - $googleCalendar = $this->doctrine->getRepository(GoogleCalendar::class)->findOneById($calendar['id']); - - //Set synchronized - $googleCalendar->setSynchronized($synchronized); - - //Queue google calendar save - $manager->persist($googleCalendar); - - //Flush to get the ids - $manager->flush(); - } - } - } - - //Return success - return self::SUCCESS; - } - - /** - * Delete event - * - * @param string $calendar The calendar mail - * @param Event $event The google event instance - * @return void - */ - function delete(string $calendar, Event $event): int { - //Get cache events - $cacheEvents = $this->item->get(); - - //Get event id - $eid = $event->getId(); - - //Delete the event - $this->service->events->delete($calendar, $eid); - - //Set sid - $sid = intval(substr($event->getId(), strlen($this->prefix))); - - //Remove from events and cache events - unset($cacheEvents[$sid]); - - //Set cache events - $this->item->set($cacheEvents); - - //Save cache item - $this->cache->saveDeferred($this->item); - - //Return session id - return $sid; - } - - /** - * Fill event - * - * TODO: add domain based/calendar mail specific templates ? - * - * @param array $session The session instance - * @param ?Event $event The event instance - * @return Event The filled event - */ - function fill(array $session, ?Event $event = null): Event { - //Init private properties - $private = [ - 'id' => $session['id'], - 'domain' => $this->domain, - 'updated' => $session['modified']->format(\DateTime::ISO8601) - ]; - - //Init shared properties - //TODO: validate for constraints here ??? https://developers.google.com/calendar/api/guides/extended-properties - //TODO: drop shared as unused ??? - $shared = [ - 'gps' => $session['l_latitude'].','.$session['l_longitude'] - ]; - - //Init source - $source = new EventSource( - [ - 'title' => $this->translator->trans('%dance% %id% by %pseudonym%', ['%id%' => $session['id'], '%dance%' => $this->translator->trans($session['ad_name'].' '.lcfirst($session['ad_type'])), '%pseudonym%' => $session['au_pseudonym']]).' '.$this->translator->trans('at '.$session['l_title']), - 'url' => $this->router->generate('rapsysair_session_view', ['id' => $session['id'], '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) - ] - ); - - //Init location - $description = 'Emplacement :'."\n".$this->translator->trans($session['l_description']); - $shared['location'] = strip_tags($this->translator->trans($session['l_description'])); - - //Add description when available - if(!empty($session['p_description'])) { - $description .= "\n\n".'Description :'."\n".strip_tags(preg_replace('!([^<]+)!', '\1', $this->markdown->convert(strip_tags($session['p_description'])))); - $shared['description'] = $this->markdown->convert(strip_tags($session['p_description'])); - } - - //Add class when available - if (!empty($session['p_class'])) { - $description .= "\n\n".'Classe :'."\n".$session['p_class']; - $shared['class'] = $session['p_class']; - } - - //Add contact when available - if (!empty($session['p_contact'])) { - $description .= "\n\n".'Contact :'."\n".$session['p_contact']; - $shared['contact'] = $session['p_contact']; - } - - //Add donate when available - if (!empty($session['p_donate'])) { - $description .= "\n\n".'Contribuer :'."\n".$session['p_donate']; - $shared['donate'] = $session['p_donate']; - } - - //Add link when available - if (!empty($session['p_link'])) { - $description .= "\n\n".'Site :'."\n".$session['p_link']; - $shared['link'] = $session['p_link']; - } - - //Add profile when available - if (!empty($session['p_profile'])) { - $description .= "\n\n".'Réseau social :'."\n".$session['p_profile']; - $shared['profile'] = $session['p_profile']; - } - - //Set properties - $properties = new EventExtendedProperties( - [ - //Set private property - 'private' => $private, - //Set shared property - 'shared' => $shared - ] - ); - - //Without event - if ($event === null) { - //Init event - $event = new Event( - [ - //Id must match /^[a-v0-9]{5,}$/ - //XXX: see https://developers.google.com/calendar/api/v3/reference/events/insert#id - 'id' => $this->prefix.$session['id'], - 'summary' => $source->getTitle(), - 'description' => $description, - 'status' => empty($session['a_canceled'])?'confirmed':'cancelled', - 'location' => implode(' ', [$session['l_address'], $session['l_zipcode'], $session['l_city']]), - 'source' => $source, - 'extendedProperties' => $properties, - //TODO: colorId ? - //TODO: attendees[] ? - 'start' => [ - 'dateTime' => $session['start']->format(\DateTime::ISO8601) - ], - 'end' => [ - 'dateTime' => $session['stop']->format(\DateTime::ISO8601) - ] - ] - ); - //With event - } else { - //Set summary - $event->setSummary($source->getTitle()); - - //Set description - $event->setDescription($description); - - //Set status - $event->setStatus(empty($session['a_canceled'])?'confirmed':'cancelled'); - - //Set location - $event->setLocation(implode(' ', [$session['l_address'], $session['l_zipcode'], $session['l_city']])); - - //Get source - #$eventSource = $event->getSource(); - - //Update source title - #$eventSource->setTitle($source->getTitle()); - - //Update source url - #$eventSource->setUrl($source->getUrl()); - - //Set source - $event->setSource($source); - - //Get extended properties - #$extendedProperties = $event->getExtendedProperties(); - - //Update private - #$extendedProperties->setPrivate($properties->getPrivate()); - - //Update shared - #$extendedProperties->setShared($properties->getShared()); - - //Set properties - $event->setExtendedProperties($properties); - - //TODO: colorId ? - //TODO: attendees[] ? - - //Set start - $start = $event->getStart(); - - //Update start datetime - $start->setDateTime($session['start']->format(\DateTime::ISO8601)); - - //Set end - $end = $event->getEnd(); - - //Update stop datetime - $end->setDateTime($session['stop']->format(\DateTime::ISO8601)); - } - - //Return event - return $event; - } - - /** - * Insert event - * - * @param string $calendar The calendar mail - * @param array $session The session instance - * @return void - */ - function insert(string $calendar, array $session): void { - //Get event - $event = $this->fill($session); - - //Get cache events - $cacheEvents = $this->item->get(); - - //Insert in cache event - $cacheEvents[$session['id']] = $this->service->events->insert($calendar, $event); - - //Set cache events - $this->item->set($cacheEvents); - - //Save cache item - $this->cache->saveDeferred($this->item); - } - - /** - * Update event - * - * @param string $calendar The calendar mail - * @param Event $event The google event instance - * @param array $session The session instance - * @return int The session id - */ - function update(string $calendar, Event $event, array $session): int { - //Get event - $event = $this->fill($session, $event); - - //Get cache events - $cacheEvents = $this->item->get(); - - //Update in cache events - $cacheEvents[$session['id']] = $this->service->events->update($calendar, $event->getId(), $event); - - //Set cache events - $this->item->set($cacheEvents); - - //Save cache item - $this->cache->saveDeferred($this->item); - - //Return session id - return $session['id']; - } -}