Mon CV

Synchroniser un fichier ics (iCal) avec un calendrier Google Agenda

17 octobre 2013

Petite problématique du jour : comment accéder à un calendrier ics depuis Google Agenda ?

C’est simple, il suffit de suivre les étapes proposées par Google :

  1. Cliquez sur la flèche déroulante à droite de Autres agendas sur votre compte Google Agenda,
  2. Choisissez “Ajouter par URL” dans ce menu,
  3. Entrez l’URL du calendrier auquel vous souhaitez vous abonner,
  4. Cliquez sur le bouton Ajouter un agenda. L’agenda apparaît dans la section Autres agendas de la liste affichée à gauche.

Sauf que ça c’est en théorie … car big brother est très sensible ! Il ne pourra pas se connecter par exemple sur des URL fournies en https avec un certificat auto-signé, il a régulièrement des problèmes d’encodage des fichiers …

J’ai donc cherché à contourner le problème, en fouillant dans les entrailles de GitHub et Google Code. Et on y trouve pas mal de choses !

En l’occurrence, mon choix s’est arrêté sur un script en Perl proposé par David Precious.

J’ai fait quelques toutes petites modifications sur le code original (disponible sur GitHub) pour :

  • Pouvoir utiliser plusieurs comptes Google Agendas distincts,
  • Ajouter un paramètre dans l’utilisation de l’API de Google qui ne renvoie par défaut que 25 résultats : get_events ('max-results' => 99999).

Il faudrait encore faire un peu de tuning pour mieux gérer les évènements en journée entière, si quelqu’un se sent le courage de mettre un peu les mains dans le code.

Avant d’utiliser le script

Pour commencer il vous faut installer les librairies (modules) Perl nécessaires au bon fonctionnement de notre script. Tout va se faire en utilisant l’archive CPAN.
Sous un Unix (donc OS X également), ouvrez votre terminal et lancez :

sudo perl -MCPAN -e shell
## Entrez votre mot de passe
cpan[1]> install CPAN
cpan[2]> reload cpan
cpan[3]> exit
## Vous retrouvez votre console Shell ...
sudo perl -MCPAN -e shell
## Entrez votre mot de passe
cpan[1]> install Net::Google::Calendar
cpan[2]> install Net::Netrc
cpan[3]> install iCal::Parser
cpan[4]> install use LWP::Simple
cpan[5]> install Getopt::Long
cpan[6]> exit

Pensez à valider l’installation de toutes les dépendances nécessaires.

Utilisez ensuite la commande git clone pour récupérer le script à l’emplacement nécessaire (ici /usr/local/bin/kerio-ics-to-gcal/).

cd /usr/local/bin
git clone https://github.com/yvangodard/ical-to-google-calendar.git
mv /usr/local/bin/ical-to-google-calendar /usr/local/bin/kerio-ics-to-gcal/
chmod -R 750 /usr/local/bin/kerio-ics-to-gcal/

Si vous n’avez pas accès aux commandes git clone, installez-les

  • avec sudo apt-get install git-core depuis votre Debian ou Ubuntu, etc.,
  • avec GitHub Mac pour OS X 10.7 et suivants,
  • avec cet installeur pour les OS X antérieurs.

Utiliser le script …

… entrer les informations de connexion dans le fichier ~/.netrc

Rien de plus simple, il vous faut tout d’abord créer un fichier .netrc dans le répertoire home de l’utilisateur qui va lancer le script. Ce fichier contiendra les informations de connexion à vos comptes google agenda. Vous  pouvez en entrer autant que nécessaire.

Depuis votre terminal, en étant connecté avec l’utilisateur qui va lancer le script, faites nano ~./netrc pour éditer ce fichier.

Pour chaque compte à configurer, il vous faut entrer trois lignes, commençant par machine, login, password.

  • machine nomducompte
  • login monadresse@gcalendar.com
  • password monsupermotdepasse

Pour configurer deux comptes votre fichier devrait ressembler à ceci :

machine nomducompte
login monadresse@gcalendar.com
password monsupermotdepasse

machine moncompte2.agenda.google
login monadresse2@u.to
password mondeuxiememotdepassesupersolide

Une fois que vous avez entré les informations faites un petit [CTRL] + X pour sortir, et n’oubliez pas de valider l’enregistrement par Yes puis [Enter].

Ensuite, pour sécuriser un peu ce fichier sensible, n’oubliez pas de faire sudo chmod 600 ~/.netrc.

… appeler votre script

Pour appeler votre script faites :

cd /usr/local/bin/kerio-ics-to-gcal/
./ical-to-gcal.pl --ical_url=ical_url --configmachine=nomducompte --calendar='Calendar Name'

Les trois paramètres sont importants :

  • ical_url renvoie sur l’URL du fichier ics à synchroniser, au format http://blabla.fr/file.ics par exemple
  • configmachine permet de savoir quels identifiants de connexion utiliser depuis votre fichier ~/.netrc
  • calendar permet de savoir sur quel calendrier, dans votre compte Google Agenda, il faut travailler (ce calendrier doit exister préalablement).

Le code du script est normalement suffisamment commenté pour que vous soyez prévenu en cas d’erreur ou que vous puissiez le modifier :

#!/usr/bin/env perl

# ical-to-gcal
# A quick script to fetch and parse an iCal/vcal feed, and update a named Google Calendar.
#
# See the README in the repo for more details on why and how to use it:
# https://github.com/yvangodard/ical-to-google-calendar
#
# Original script by David Precious <davidp@preshweb.co.uk>
# https://github.com/bigpresh/ical-to-google-calendar

use strict;
use Net::Google::Calendar;
use Net::Netrc;
use iCal::Parser;
use LWP::Simple;
use Getopt::Long;

my ($calendar, $ical_url, $configmachine);
Getopt::Long::GetOptions(
    "calendar|c=s"      => \$calendar,
    "ical_url|ical|i=s" => \$ical_url,
    "configmachine|i=s" => \$configmachine,
) or die "Failed to parse options";

if (!$ical_url) {
    die "An iCal URL must be provided (--ical_url=....)";
}
if (!$calendar) {
    die "You must specify the calendar name (--calendar=...)";
}
if (!$configmachine) {
    die "You must specify the configmachine to use in  ~/.netrc (--configmachine=...)";
}

my $ical_data = LWP::Simple::get($ical_url)
    or die "Failed to fetch $ical_url";

my $ic = iCal::Parser->new;

my $ical = $ic->parse_strings($ical_data)
    or die "Failed to parse iCal data";

# We get events keyed by year, month, day - we just want a flat list of events
# to walk through.  Do this keyed by the event ID, so that multiple-day events
# are handled appropriately.  We'll want this hash anyway to do a pass through
# all events on the Google Calendar, removing any that are no longer in the
# iCal feed.

my %ical_events;
for my $year (keys %{ $ical->{events} }) {
    for my $month (keys %{ $ical->{events}{$year} }) {
        for my $day (keys %{ $ical->{events}{$year}{$month} }) {
            for my $event_uid (keys %{ $ical->{events}{$year}{$month}{$day} }) {
                $ical_events{ $event_uid }
                    = $ical->{events}{$year}{$month}{$day}{$event_uid};
            }
        }
    }
}

# Right, we can now walk through each event in $events
for my $event_uid (keys %ical_events) {
    my $event = $ical_events{$event_uid};
    printf "$event_uid (%s at %s)\n",
        @$event{ qw (SUMMARY LOCATION) };
}


# Get our login details, and find the Google calendar in question:
my $mach = Net::Netrc->lookup($configmachine)
    or die "No login details for $configmachine in ~/.netrc";
my ($user, $pass) = $mach->lpa;


my $gcal = Net::Google::Calendar->new;
$gcal->login($user, $pass)
    or die "Google Calendar login failed";

my ($desired_calendar) = grep { $_->title eq $calendar } $gcal->get_calendars;

if (!$desired_calendar) {
    die "No calendar named $calendar found!";
}
$gcal->set_calendar($desired_calendar);

# Fetch all events from this calendar, parse out the ical feed's UID and whack
# them in a hash keyed by the UID; if that UID no longer appears in the ical
# feed, it's one to delete.

my %gcal_events;

gcal_event:
for my $event ($gcal->get_events ('max-results' => 99999)) {
    my ($ical_uid) = $event->content->body =~ m{\[ical_imported_uid:(.+)\]};

    # If there's no ical uid, we presumably didn't create this, so leave it
    # alone
    if (!$ical_uid) {
        warn sprintf "Event %s (%s) ignored as it has no "
            . "ical_imported_uid property",
            $event->id,
            $event->title;
        next gcal_event;
    }

    # OK, if this event didn't appear in the iCal feed, it has been deleted at
    # the other end, so we should delete it from our Google calendar:
    if (!$ical_events{$ical_uid}) {
        printf "Deleting event %s (%s) (no longer found in iCal feed)\n",
            $event->id, $event->title;
        $gcal->delete_entry($event)
            or warn "Failed to delete an event from Google Calendar";
    }

    # Now check for any differences, and update if required

    # Remember that we found this event, so we can refer to it when looking for
    # events we need to create
    $gcal_events{$ical_uid} = $event;
}


# Now, walk through the ical events we found, and create/update Google Calendar
# events
for my $ical_uid (keys %ical_events) {
    
    my ($method, $gcal_event);

    if (exists $gcal_events{$ical_uid}) {
        $gcal_event = $gcal_events{$ical_uid};
        $method = 'update_entry';
        print "Need to update ical event $ical_uid in gcal\n";
    } else {
        $gcal_event = Net::Google::Calendar::Entry->new;
        $method = 'add_entry';
        print "Need to create new gcal event for $ical_uid\n";
    }

    my $ical_event = $ical_events{$ical_uid};
    $gcal_event->title(    $ical_event->{SUMMARY}  );
    $gcal_event->location( $ical_event->{LOCATION} );
    $gcal_event->when( $ical_event->{DTSTART}, $ical_event->{DTEND} );

    if ($method eq 'add_entry') {
        $gcal_event->content("[ical_imported_uid:$ical_uid]");
    }

    $gcal->$method($gcal_event)
        or warn "Failed to $method for $ical_uid";
}

Il ne suffit plus que de programmer cela dans votre crontab ou LaunchDaemons pour réaliser une actualisation toutes les demi-heures par exemple.

Posted in Apple et Macintosh, Unix et LinuxTags: