diff --git a/library/calendar_sync.py b/library/calendar_sync.py index fe07258..d1a2d20 100644 --- a/library/calendar_sync.py +++ b/library/calendar_sync.py @@ -3,8 +3,24 @@ from ansible.module_utils.basic import AnsibleModule from caldav import DAVClient import vobject +import requests +from urllib.parse import urlparse -def connect_calendar(url, user=None, password=None, verify_ssl=True): +def fetch_ics_events(url, user=None, password=None): + """Lädt Events aus einer ICS-URL""" + try: + auth = (user, password) if user and password else None + response = requests.get(url, auth=auth) + response.raise_for_status() + + ics_data = response.text + cal = vobject.readOne(ics_data) + return [comp for comp in cal.components() if comp.name == 'VEVENT'] + except Exception as e: + raise Exception(f"ICS-Download fehlgeschlagen: {str(e)}") + +def connect_caldav(url, user=None, password=None, verify_ssl=True): + """Stellt Verbindung zum CalDAV-Kalender her""" try: client = DAVClient( url, @@ -12,53 +28,67 @@ def connect_calendar(url, user=None, password=None, verify_ssl=True): password=password, ssl_verify_cert=verify_ssl ) - calendar = client.calendar(url=url) - # Test-Zugriff um sicherzustellen, dass der Kalender existiert - calendar.get_properties(['displayname']) - return calendar + # Kalender-URL aus Pfad extrahieren + path = urlparse(url).path + principal = client.principal() + for calendar in principal.calendars(): + if path in calendar.url: + return calendar + raise Exception("Kalender nicht gefunden") except Exception as e: - raise Exception(f"Kalenderverbindung fehlgeschlagen: {str(e)}") - -def get_event_uid_map(calendar): - uid_map = {} - for event in calendar.events(): - try: - cal = vobject.readOne(event.data) - uid = cal.vevent.uid.value - uid_map[uid] = event - except Exception as e: - continue # Fehlerhafte Events überspringen - return uid_map - -def sync_calendars(source_cal, target_cal): - src_events = get_event_uid_map(source_cal) - tgt_events = get_event_uid_map(target_cal) + raise Exception(f"CalDAV-Verbindungsfehler: {str(e)}") +def sync_ics_to_caldav(module): + """Haupt-Synchronisationslogik""" + # ICS-Events holen + ics_events = fetch_ics_events( + module.params['source_url'], + module.params['source_user'], + module.params['source_password'] + ) + + # CalDAV-Ziel verbinden + target_cal = connect_caldav( + module.params['target_url'], + module.params['target_user'], + module.params['target_password'], + module.params['verify_ssl'] + ) + + # Existierende Events mappen + existing_events = { + event.icalendar_component['UID']: event + for event in target_cal.events() + } + changed = False - added, updated, removed = [], [], [] - - # Neue/aktualisierte Events - for uid, src_event in src_events.items(): - tgt_event = tgt_events.get(uid) - src_data = src_event.data + results = {'added': [], 'updated': [], 'removed': []} + + # Neue/aktualisierte Events syncen + for vevent in ics_events: + uid = str(vevent['UID'].value) + ical_data = vevent.serialize() - if not tgt_event: - target_cal.add_event(src_data) - added.append(uid) + if uid not in existing_events: + target_cal.add_event(ical_data) + results['added'].append(uid) changed = True - elif src_data != tgt_event.data: - tgt_event.data = src_data - tgt_event.save() - updated.append(uid) + else: + # Nur aktualisieren wenn unterschiedlich + if ical_data != existing_events[uid].data: + existing_events[uid].data = ical_data + existing_events[uid].save() + results['updated'].append(uid) + changed = True + + # Optionale Löschung nicht vorhandener Events + if module.params.get('purge'): + for uid in set(existing_events.keys()) - {str(e['UID'].value) for e in ics_events}: + existing_events[uid].delete() + results['removed'].append(uid) changed = True - - # Entfernte Events löschen - for uid in set(tgt_events.keys()) - set(src_events.keys()): - tgt_events[uid].delete() - removed.append(uid) - changed = True - - return changed, added, updated, removed + + return changed, results def run_module(): module_args = dict( @@ -68,52 +98,17 @@ def run_module(): target_url=dict(type='str', required=True), target_user=dict(type='str', required=False, default=None), target_password=dict(type='str', required=False, no_log=True, default=None), - verify_ssl=dict(type='bool', required=False, default=True) - ) - - module = AnsibleModule( - argument_spec=module_args, - supports_check_mode=False - ) - - result = dict( - changed=False, - added=[], - updated=[], - removed=[] + verify_ssl=dict(type='bool', default=True), + purge=dict(type='bool', default=False) ) + module = AnsibleModule(argument_spec=module_args, supports_check_mode=False) + try: - # Kalender verbinden - source_cal = connect_calendar( - url=module.params['source_url'], - user=module.params['source_user'], - password=module.params['source_password'], - verify_ssl=module.params['verify_ssl'] - ) - - target_cal = connect_calendar( - url=module.params['target_url'], - user=module.params['target_user'], - password=module.params['target_password'], - verify_ssl=module.params['verify_ssl'] - ) - - # Synchronisation durchführen - changed, added, updated, removed = sync_calendars(source_cal, target_cal) - - result.update({ - 'changed': changed, - 'added': added, - 'updated': updated, - 'removed': removed, - 'msg': f"Synchronisiert: +{len(added)}/~{len(updated)}/-{len(removed)}" - }) - + changed, results = sync_ics_to_caldav(module) + module.exit_json(changed=changed, **results) except Exception as e: - module.fail_json(msg=f"Fehler bei der Synchronisation: {str(e)}", **result) - - module.exit_json(**result) + module.fail_json(msg=str(e)) if __name__ == '__main__': run_module() \ No newline at end of file