namespace Rapsys\AirBundle\Command;
use Doctrine\Persistence\ManagerRegistry;
-use Symfony\Component\Cache\Adapter\FilesystemAdapter;
+
+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\DependencyInjection\ContainerInterface;
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' google calendar
+ */
class CalendarCommand extends Command {
/**
- * Creates new calendar command
+ * Set description
*
- * @param ManagerRegistry $doctrine The doctrine instance
- * @param string $locale The default locale
- * @param RouterInterface $router The router instance
- * @param SluggerUtil $slugger The slugger instance
- * @param TranslatorInterface $translator The translator instance
- * @param string $namespace The cache namespace
- * @param int $lifetime The cache lifetime
- * @param string $path The cache path
+ * Shown with bin/console list
*/
- public function __construct(protected ManagerRegistry $doctrine, protected string $locale, protected RouterInterface $router, protected SluggerUtil $slugger, protected TranslatorInterface $translator, protected string $namespace, protected int $lifetime, protected string $path) {
- //Call parent constructor
- parent::__construct($this->doctrine, $this->locale, $this->router, $this->slugger, $this->translator);
- }
+ protected string $description = 'Synchronize sessions in users\' calendar';
/**
- * Configure attribute command
+ * Set help
+ *
+ * Shown with bin/console --help rapsysair:calendar
*/
- protected function configure() {
- //Configure the class
- $this
- //Set name
- ->setName('rapsysair:calendar')
- //Set description shown with bin/console list
- ->setDescription('Synchronize sessions in calendar')
- //Set description shown with bin/console --help airlibre:attribute
- ->setHelp('This command synchronize sessions in google calendar');
- }
+ protected string $help = 'This command synchronize sessions in users\' google calendar';
/**
- * Process the attribution
+ * Set domain
*/
- protected function execute(InputInterface $input, OutputInterface $output): int {
- //Compute period
- $period = new \DatePeriod(
- //Start from last week
- new \DateTime('-1 week'),
- //Iterate on each day
- new \DateInterval('P1D'),
- //End with next 2 week
- new \DateTime('+2 week')
- );
+ protected string $domain;
- //Retrieve events to update
- $sessions = $this->doctrine->getRepository(Session::class)->fetchAllByDatePeriod($period, $this->locale);
+ /**
+ * Set item
+ *
+ * Cache item instance
+ */
+ protected ItemInterface $item;
- //Markdown converted instance
- $markdown = new DefaultMarkdown;
+ /**
+ * Set prefix
+ */
+ protected string $prefix;
- //Retrieve cache object
- //XXX: by default stored in /tmp/symfony-cache/@/W/3/6SEhFfeIW4UMDlAII+Dg
- //XXX: stored in %kernel.project_dir%/var/cache/airlibre/0/P/IA20X0K4dkMd9-+Ohp9Q
- $cache = new FilesystemAdapter($this->namespace, $this->lifetime, $this->path);
+ /**
+ * Set service
+ *
+ * Google calendar instance
+ */
+ protected Calendar $service;
- //Retrieve calendars
- $cacheCalendars = $cache->getItem('calendars');
+ /**
+ * {@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);
- //Without calendars
- if (!$cacheCalendars->isHit()) {
- //Return failure
- return self::FAILURE;
- }
+ //Replace google client redirect uri
+ $this->google->setRedirectUri($this->router->generate($this->google->getRedirectUri(), [], UrlGeneratorInterface::ABSOLUTE_URL));
+ }
- //Retrieve calendars
- $calendars = $cacheCalendars->get();
-
- //XXX: calendars content
- #var_export($calendars);
-
- //Check expired token
- foreach($calendars as $clientId => $client) {
- //Get google client
- $googleClient = new \Google\Client(['application_name' => $client['project'], 'client_id' => $clientId, 'client_secret' => $client['secret'], 'redirect_uri' => $client['redirect']]);
-
- //Iterate on each tokens
- foreach($client['tokens'] as $tokenId => $token) {
- //Set token
- $googleClient->setAccessToken(
- [
- 'access_token' => $tokenId,
- 'refresh_token' => $token['refresh'],
- 'expires_in' => $token['expire'],
- 'scope' => $token['scope'],
- 'token_type' => $token['type'],
- 'created' => $token['created']
- ]
- );
-
- //With expired token
- if ($exp = $googleClient->isAccessTokenExpired()) {
- //Refresh token
- if (($refreshToken = $googleClient->getRefreshToken()) && ($googleToken = $googleClient->fetchAccessTokenWithRefreshToken($refreshToken)) && empty($googleToken['error'])) {
- //Add refreshed token
- $calendars[$clientId]['tokens'][$googleToken['access_token']] = [
- 'calendar' => $token['calendar'],
- 'prefix' => $token['prefix'],
- 'refresh' => $googleToken['refresh_token'],
- 'expire' => $googleToken['expires_in'],
- 'scope' => $googleToken['scope'],
- 'type' => $googleToken['token_type'],
- 'created' => $googleToken['created']
- ];
-
- //Remove old token
- unset($calendars[$clientId]['tokens'][$tokenId]);
- } else {
- //Drop token
- unset($calendars[$clientId]['tokens'][$tokenId]);
-
- //Without tokens
- if (empty($calendars[$clientId]['tokens'])) {
- //Drop client
- unset($calendars[$clientId]);
- }
+ /**
+ * Process the attribution
+ */
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ //Get domain
+ $this->domain = $this->router->getContext()->getHost();
- //Save calendars
- $cacheCalendars->set($calendars);
+ //Get manager
+ $manager = $this->doctrine->getManager();
- //Save calendar
- $cache->save($cacheCalendars);
+ //Convert from any to latin, then to ascii and lowercase
+ $trans = \Transliterator::create('Any-Latin; Latin-ASCII; Lower()');
- //Drop token and report
- //XXX: submit app to avoid expiration
- //XXX: see https://console.cloud.google.com/apis/credentials/consent?project=calendar-317315
- echo 'Token '.$tokenId.' for calendar '.$token['calendar'].' has expired and is not refreshable'."\n";
+ //Replace every non alphanumeric character by dash then trim dash
+ $this->prefix = preg_replace(['/(\.[a-z0-9]+)+$/', '/[^a-v0-9]+/'], '', $trans->transliterate($this->domain));
- //Return failure
- //XXX: we want that mail and stop here
- return self::FAILURE;
- }
- }
- }
+ //With too short prefix
+ if ($this->prefix === null || strlen($this->prefix) < 4) {
+ //Throw domain exception
+ throw new \DomainException('Prefix too short: '.$this->prefix);
}
- //Save calendars
- $cacheCalendars->set($calendars);
-
- //Save calendar
- $cache->save($cacheCalendars);
-
- //Iterate on each calendar client
- foreach($calendars as $clientId => $client) {
- //Get google client
- $googleClient = new \Google\Client(['application_name' => $client['project'], 'client_id' => $clientId, 'client_secret' => $client['secret'], 'redirect_uri' => $client['redirect']]);
-
- //Iterate on each tokens
- foreach($client['tokens'] as $tokenId => $token) {
- //Set token
- $googleClient->setAccessToken(
- [
- 'access_token' => $tokenId,
- 'refresh_token' => $token['refresh'],
- 'expires_in' => $token['expire'],
- 'scope' => $token['scope'],
- 'token_type' => $token['type'],
- 'created' => $token['created']
- ]
- );
-
- //With expired token
- if ($exp = $googleClient->isAccessTokenExpired()) {
- //Last chance to skip this run
+ //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
- $googleCalendar = new \Google\Service\Calendar($googleClient);
+ //Get google calendar service
+ $this->service = new Calendar($this->google);
- //Retrieve calendar
+ //Iterate on google calendars
+ foreach($calendars = $token['calendars'] as $cid => $calendar) {
+ //Set start
+ $synchronized = null;
+
+ //Set cache key
+ $cacheKey = 'command.calendar.'.$this->slugger->short($calendar['mail']);
+
+ //XXX: TODO: remove DEBUG
+ #$this->cache->delete($cacheKey);
+
+ //Retrieve calendar events
try {
- $calendar = $googleCalendar->calendars->get($token['calendar']);
+ //Get events
+ $events = $this->cache->get(
+ //Cache key
+ //XXX: set to command.calendar.$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) {
- //Display exception
- //TODO: handle codes here https://developers.google.com/calendar/api/guides/errors
- echo 'Exception '.$e->getCode().':'.$e->getMessage().' in '.$e->getFile().' +'.$e->getLine()."\n";
- echo $e->getTraceAsString()."\n";
+ //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()?:'');
- //Return failure
- return self::FAILURE;
+ //TODO: warn user by mail ?
+
+ //Skip to next token
+ continue;
+ }
+
+ //Throw error
+ throw new \LogicException('Calendar event list failed', 0, $e);
}
- //Init events
- $events = [];
-
- //Set filters
- $filters = [
- //XXX: show even deleted event to be able to update them
- 'showDeleted' => true,
- //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', */
- ];
-
- //Retrieve event collection
- $googleEvents = $googleCalendar->events->listEvents($token['calendar'], $filters);
-
- //Iterate until reached end
- while (true) {
- //Iterate on each event
- foreach ($googleEvents->getItems() as $event) {
- //Store event by id
- if (preg_match('/^'.$token['prefix'].'([0-9]+)$/', $id = $event->getId(), $matches)) {
- $events[$matches[1]] = $event;
- //XXX: 3rd party events with id not matching prefix are skipped
- #} else {
- # echo 'Skipping '.$event->getId().':'.$event->getSummary()."\n";*/
+ //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 page token
- $pageToken = $googleEvents->getNextPageToken();
+ //Get all sessions
+ $sessions = $this->doctrine->getRepository(Session::class)->findAllByUserIdSynchronized($token['uid']);
- //Handle next page
- if ($pageToken) {
- //Replace collection with next one
- $googleEvents = $service->events->listEvents($token['calendar'], $filters+['pageToken' => $pageToken]);
- } else {
- break;
+ //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]);
+ }
}
}
- //Iterate on each session to sync
- foreach($sessions as $sessionId => $session) {
- //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 = [
- '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']),
- 'url' => $this->router->generate('rapsysair_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)
- ];
-
- //Init location
- $description = 'Emplacement :'."\n".$this->translator->trans($session['l_description']);
- $shared['location'] = $markdown->convert(strip_tags($session['l_description']));
-
- //Add description
- $description .= "\n\n".'Description :'."\n".strip_tags(preg_replace('!<a href="([^"]+)"(?: title="[^"]+")?'.'>([^<]+)</a>!', '\1', $markdown->convert(strip_tags($session['p_description']))));
- $shared['description'] = $markdown->convert(strip_tags($session['p_description']));
-
- //Add class when available
- if (!empty($session['p_class'])) {
- $shared['class'] = $session['p_class'];
- $description .= "\n\n".'Classe :'."\n".$session['p_class'];
- }
+ //Persist cache item
+ $this->cache->commit();
- //Add contact when available
- if (!empty($session['p_contact'])) {
- $shared['contact'] = $session['p_contact'];
- $description .= "\n\n".'Contact :'."\n".$session['p_contact'];
- }
+ //With synchronized
+ //XXX: only store synchronized on run without caching
+ if ($synchronized) {
+ //Get google calendar
+ $googleCalendar = $this->doctrine->getRepository(GoogleCalendar::class)->findOneById($calendar['id']);
- //Add donate when available
- if (!empty($session['p_donate'])) {
- $shared['donate'] = $session['p_donate'];
- $description .= "\n\n".'Contribuer :'."\n".$session['p_donate'];
- }
+ //Set synchronized
+ $googleCalendar->setSynchronized($synchronized);
- //Add link when available
- if (!empty($session['p_link'])) {
- $shared['link'] = $session['p_link'];
- $description .= "\n\n".'Site :'."\n".$session['p_link'];
- }
+ //Queue google calendar save
+ $manager->persist($googleCalendar);
- //Add profile when available
- if (!empty($session['p_profile'])) {
- $shared['profile'] = $session['p_profile'];
- $description .= "\n\n".'Réseau social :'."\n".$session['p_profile'];
- }
+ //Flush to get the ids
+ $manager->flush();
+ }
+ }
+ }
- //Locked session
- if (!empty($session['locked']) && $events[$sessionId]) {
- //With events
- if (!empty($event = $events[$sessionId])) {
- try {
- //Delete the event
- $googleCalendar->events->delete($token['calendar'], $event->getId());
- //Catch exception
- } catch(\Google\Service\Exception $e) {
- //Display exception
- //TODO: handle codes here https://developers.google.com/calendar/api/guides/errors
- echo 'Exception '.$e->getCode().':'.$e->getMessage().' in '.$e->getFile().' +'.$e->getLine()."\n";
- echo $e->getTraceAsString()."\n";
-
- //Return failure
- return self::FAILURE;
- }
- }
- //Without event
- } elseif (empty($events[$sessionId])) {
- //Init event
- $event = new \Google\Service\Calendar\Event(
- [
- //TODO: replace 'airlibre' with $this->config['calendar']['prefix'] when possible with prefix validating [a-v0-9]{5,}
- //XXX: see https://developers.google.com/calendar/api/v3/reference/events/insert#id
- 'id' => $token['prefix'].$sessionId,
- #'summary' => $session['au_pseudonym'].' '.$this->translator->trans('at '.$session['l_title']),
- 'summary' => $source['title'],
- #'description' => $markdown->convert(strip_tags($session['p_description'])),
- 'description' => $description,
- 'status' => empty($session['a_canceled'])?'confirmed':'cancelled',
- 'location' => implode(' ', [$session['l_address'], $session['l_zipcode'], $session['l_city']]),
- 'source' => $source,
- 'extendedProperties' => [
- 'shared' => $shared
- ],
- //TODO: colorId ?
- //TODO: attendees[] ?
- 'start' => [
- 'dateTime' => $session['start']->format(\DateTime::ISO8601)
- ],
- 'end' => [
- 'dateTime' => $session['stop']->format(\DateTime::ISO8601)
- ]
- ]
- );
-
- try {
- //Insert the event
- $googleCalendar->events->insert($token['calendar'], $event);
- //Catch exception
- } catch(\Google\Service\Exception $e) {
- //Display exception
- //TODO: handle codes here https://developers.google.com/calendar/api/guides/errors
- echo 'Exception '.$e->getCode().':'.$e->getMessage().' in '.$e->getFile().' +'.$e->getLine()."\n";
- echo $e->getTraceAsString()."\n";
-
- //Return failure
- return self::FAILURE;
- }
- // With event
- } else {
- //Set event
- $event = $events[$sessionId];
+ //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);
- //With updated event
- if ($session['updated'] >= (new \DateTime($event->getUpdated()))) {
- //Set summary
- #$event->setSummary($session['au_pseudonym'].' '.$this->translator->trans('at '.$session['l_title']));
- $event->setSummary($source['title']);
+ //Set sid
+ $sid = intval(substr($event->getId(), strlen($this->prefix)));
- //Set description
- $event->setDescription($description);
+ //Remove from events and cache events
+ unset($cacheEvents[$sid]);
- //Set status
- $event->setStatus(empty($session['a_canceled'])?'confirmed':'cancelled');
+ //Set cache events
+ $this->item->set($cacheEvents);
- //Set location
- $event->setLocation(implode(' ', [$session['l_address'], $session['l_zipcode'], $session['l_city']]));
+ //Save cache item
+ $this->cache->saveDeferred($this->item);
- //Get source
- $eventSource = $event->getSource();
+ //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)
+ ]
+ );
- //Update source title
- $eventSource->setTitle($source['title']);
+ //Init location
+ $description = 'Emplacement :'."\n".$this->translator->trans($session['l_description']);
+ $shared['location'] = strip_tags($this->translator->trans($session['l_description']));
- //Update source url
- $eventSource->setUrl($source['url']);
+ //Add description when available
+ if(!empty($session['p_description'])) {
+ $description .= "\n\n".'Description :'."\n".strip_tags(preg_replace('!<a href="([^"]+)"(?: title="[^"]+")?'.'>([^<]+)</a>!', '\1', $this->markdown->convert(strip_tags($session['p_description']))));
+ $shared['description'] = $this->markdown->convert(strip_tags($session['p_description']));
+ }
- //Set source
- #$event->setSource($source);
+ //Add class when available
+ if (!empty($session['p_class'])) {
+ $description .= "\n\n".'Classe :'."\n".$session['p_class'];
+ $shared['class'] = $session['p_class'];
+ }
- //Get extended properties
- $extendedProperties = $event->getExtendedProperties();
+ //Add contact when available
+ if (!empty($session['p_contact'])) {
+ $description .= "\n\n".'Contact :'."\n".$session['p_contact'];
+ $shared['contact'] = $session['p_contact'];
+ }
- //Update shared
- $extendedProperties->setShared($shared);
+ //Add donate when available
+ if (!empty($session['p_donate'])) {
+ $description .= "\n\n".'Contribuer :'."\n".$session['p_donate'];
+ $shared['donate'] = $session['p_donate'];
+ }
- //TODO: colorId ?
- //TODO: attendees[] ?
+ //Add link when available
+ if (!empty($session['p_link'])) {
+ $description .= "\n\n".'Site :'."\n".$session['p_link'];
+ $shared['link'] = $session['p_link'];
+ }
- //Set start
- $start = $event->getStart();
+ //Add profile when available
+ if (!empty($session['p_profile'])) {
+ $description .= "\n\n".'Réseau social :'."\n".$session['p_profile'];
+ $shared['profile'] = $session['p_profile'];
+ }
- //Update start datetime
- $start->setDateTime($session['start']->format(\DateTime::ISO8601));
+ //Set properties
+ $properties = new EventExtendedProperties(
+ [
+ //Set private property
+ 'private' => $private,
+ //Set shared property
+ 'shared' => $shared
+ ]
+ );
- //Set end
- $end = $event->getEnd();
+ //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());
- //Update stop datetime
- $end->setDateTime($session['stop']->format(\DateTime::ISO8601));
+ //Set description
+ $event->setDescription($description);
- try {
- //Update the event
- $updatedEvent = $googleCalendar->events->update($token['calendar'], $event->getId(), $event);
- //Catch exception
- } catch(\Google\Service\Exception $e) {
- //Display exception
- //TODO: handle codes here https://developers.google.com/calendar/api/guides/errors
- echo 'Exception '.$e->getCode().':'.$e->getMessage().' in '.$e->getFile().' +'.$e->getLine()."\n";
- echo $e->getTraceAsString()."\n";
+ //Set status
+ $event->setStatus(empty($session['a_canceled'])?'confirmed':'cancelled');
- //Return failure
- return self::FAILURE;
- }
- }
+ //Set location
+ $event->setLocation(implode(' ', [$session['l_address'], $session['l_zipcode'], $session['l_city']]));
- //Drop from events array
- unset($events[$sessionId]);
- }
- }
+ //Get source
+ #$eventSource = $event->getSource();
- //Remaining events to drop
- foreach($events as $eventId => $event) {
- //Non canceled events
- if ($event->getStatus() == 'confirmed') {
- try {
- //Delete the event
- $googleCalendar->events->delete($token['calendar'], $event->getId());
- //Catch exception
- } catch(\Google\Service\Exception $e) {
- //Display exception
- //TODO: handle codes here https://developers.google.com/calendar/api/guides/errors
- echo 'Exception '.$e->getCode().':'.$e->getMessage().' in '.$e->getFile().' +'.$e->getLine()."\n";
- echo $e->getTraceAsString()."\n";
-
- //Return failure
- return self::FAILURE;
- }
- }
- }
- }
+ //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 success
- return self::SUCCESS;
+ //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'];
}
}