fr en

Importer périodiquement un flux ical dans owncloud

Posted on 2015-01-09 in Programmation

J'ai toujours trouvé dommage que owncloud ne soit pas capable d'importer périodiquement des calendriers que l'on trouve sur le web. Dans mon cas, mon emploi du temps est un flux ical régulièrement mis à jour et j'aimerais bien que owncloud soit capable de l'importer. Je me suis finalement résigné à faire mon propre script pour régler ce problème.

Je pensais initialement le faire en Bash et utiliser curl pour faire les requêtes vers owncloud. Cependant, un flux ical peut contenir des sauts de lignes et les tenter de gérer correctement m'a fait souffrir. Du coup, je me suis rabattu sur python et son excellente bibliothèque requests.

Concrètement, ce script :

  • lit dans ~/.owncloud l'adresse et les identifiants du serveur owncloud,
  • récupère les événements définis dans les flux icals que je lui demande de synchroniser. Ces événements sont stockés dans une instance de la classe Vevent crée à cet effet. J'ai ajouté la possibilité de filtrer certains événements (pour exclure des cours que je n'ai pas),
  • supprime les événements présents dans owncloud mais absent des flux icals,
  • poste (crée ou met à jour) les événements.

Il ne reste plus qu'à mettre le script dans un cron pour faire l'import périodiquement.

Normalement le script est assez simple et clair pour être facilement compréhensible. En cas de problème, postez un commentaire.

Vous pouvez aussi télécharger ce script et le voir sur le dépôt mercurial.

  1 #!/usr/bin/python3
  2 
  3 import requests
  4 from requests.auth import HTTPBasicAuth
  5 import re
  6 import xml.etree.ElementTree as ET
  7 
  8 
  9 class Vevent:
 10     UID_REGEXP = re.compile('^UID:.*')
 11     SUMMARY_REGEXP = re.compile('^SUMMARY:.*')
 12 
 13     def __init__(self, lines):
 14         self._lines = lines
 15         self._uid = self._get_line_from_regexp(self.UID_REGEXP)
 16         self._summary = self._get_line_from_regexp(self.SUMMARY_REGEXP)
 17 
 18     def _get_line_from_regexp(self, regexp):
 19         for line in self._lines:
 20             if regexp.match(line):
 21                 index = self._lines.index(line)
 22                 return line + self._lines[index + 1]
 23 
 24     def get_vevent_for_put(self):
 25         return '\r\n'.join(self._lines)
 26 
 27     def get_as_vcal_for_put(self):
 28         str_vevent = 'BEGIN:VCALENDAR\r\n' + self.get_vevent_for_put() \
 29                      + '\r\nEND:VCALENDAR'
 30         # If you experience problem with the line below, try
 31         # return strvevent
 32         # instead
 33         return str_vevent.encode('utf-8')
 34 
 35     @property
 36     def uid(self):
 37         return self._uid
 38 
 39     @property
 40     def summary(self):
 41         return self._summary
 42 
 43     def __repr__(self):
 44         return '{}\n{}'.format(self._uid, self._summary)
 45 
 46 
 47 def get_login():
 48     owncloud_omis_url = ''
 49     login = ''
 50     password = ''
 51     with open('/home/jenselme/.owncloud') as owncloud:
 52         owncloud_omis_url, login, password = [line for line in
 53                                               owncloud.read().split('\n') if line]
 54     return owncloud_omis_url, login, password
 55 
 56 def fetch_all_vevents(urls, filter_dict):
 57     vevents = []
 58     for name, url in urls.items():
 59         current_vevents = get_vevents(url)
 60         if name in filter_dict:
 61             current_vevents = [vevent for vevent in current_vevents
 62                                if filter_dict[name].match(vevent.summary)]
 63         vevents.extend(current_vevents)
 64     return vevents
 65 
 66 def get_vevents(get_url):
 67     calendar = requests.get(get_url).content.decode('utf-8')
 68     calendar_lines = [line for line in calendar.split('\r\n') if line]
 69     # Remove VCALENDAR lines
 70     del calendar_lines[0]
 71     del calendar_lines[-1]
 72 
 73     vevent_lines = []
 74     vevents = []
 75     for line in calendar_lines:
 76         vevent_lines.append(line)
 77         if VEVENT_END.match(line) and vevent_lines:
 78             vevents.append(Vevent(vevent_lines))
 79             vevent_lines = []
 80     return vevents
 81 
 82 def delete_all_removed_vevents(fetch_vevents, destination_url, request_params):
 83     destination_uids = get_destination_calendar_uids(destination_url, request_params)
 84     source_uids = [vevent.uid for vevent in fetch_vevents]
 85     removed_from_source_uids = [uid for uid in destination_uids if uid not in source_uids]
 86     delete_all(removed_from_source_uids, destination_url, request_params)
 87 
 88 def get_destination_calendar_uids(destination_url, request_params):
 89     resp = requests.request('PROPFIND', destination_url, **request_params)
 90     xml_str = resp.content.decode('utf-8')
 91     root = ET.fromstring(xml_str)
 92     hrefs_uid = []
 93     for response in root:
 94         for child in response:
 95             if child.tag == '{DAV:}href':
 96                 hrefs_uid.append(child.text)
 97     uids = []
 98     for href in hrefs_uid:
 99         uid = href.split('/')[-1]
100         if uid:
101             uids.append(uid)
102     return uids
103 
104 def delete_all(uids, url, request_params):
105     for uid in uids:
106         delete_url = '{}/{}'.format(url, uid)
107         requests.delete(delete_url, **request_params)
108 
109 def put_all_vevents_as_vcalendars(vevents, calendar_put_url, request_params):
110     for vevent in vevents:
111         put_url = '{}/{}.ics'.format(calendar_put_url, vevent.uid)
112         data = vevent.get_as_vcal_for_put()
113         requests.put(put_url, data=data, **request_params)
114 
115 
116 if __name__ == '__main__':
117     # Global variables
118     VEVENT_END = re.compile('^END:VEVENT$')
119     SOCIOLOGIE_ORGANISATION = re.compile('.*Sociologie des Organisations.*')
120     EDT_FETCH_URLS = {'omis-org': 'https://ade6.centrale-marseille.fr/jsp/custom/modules/plannings/anonymous_cal.jsp?resources=804&projectId=15&calType=ical&firstDate=2015-01-06&lastDate=2015-06-30',
121     'tc-electif': 'https://ade6.centrale-marseille.fr/jsp/custom/modules/plannings/anonymous_cal.jsp?resources=1043&projectId=15&calType=ical&firstDate=2014-11-24&lastDate=2015-04-30',
122     'ENT': 'https://ade6.centrale-marseille.fr/jsp/custom/modules/plannings/anonymous_cal.jsp?resources=208&projectId=15&calType=ical&firstDate=2014-10-20&lastDate=2015-05-31',
123     'omis-gB': 'https://ade6.centrale-marseille.fr/jsp/custom/modules/plannings/anonymous_cal.jsp?resources=507&projectId=15&calType=ical&firstDate=2014-09-15&lastDate=2015-05-31',
124     'tc2-td3': 'https://ade6.centrale-marseille.fr/jsp/custom/modules/plannings/anonymous_cal.jsp?resources=194&projectId=15&calType=ical&firstDate=2014-09-01&lastDate=2015-06-30',
125     'omis-i': 'https://ade6.centrale-marseille.fr/jsp/custom/modules/plannings/anonymous_cal.jsp?resources=814&projectId=15&calType=ical&firstDate=2014-09-01&lastDate=2015-05-31'}
126 
127     owncloud_omis_url, login, password = get_login()
128 
129     request_params = {'verify': False, 'auth': HTTPBasicAuth(login, password)}
130     fetched_vevents = fetch_all_vevents(EDT_FETCH_URLS, {'omis-org': SOCIOLOGIE_ORGANISATION})
131     delete_all_removed_vevents(fetched_vevents, owncloud_omis_url, request_params)
132     put_all_vevents_as_vcalendars(fetched_vevents, owncloud_omis_url, request_params)