diff --git a/library/calendar_sync.py b/library/calendar_sync.py new file mode 100644 index 0000000..47ac5ab --- /dev/null +++ b/library/calendar_sync.py @@ -0,0 +1,159 @@ +#!/usr/bin/env python3 + +from ansible.module_utils.basic import AnsibleModule +from caldav import DAVClient, Calendar +import vobject +import requests +import logging + +# Logging für Debugging einrichten +logging.basicConfig(level=logging.WARNING) + +def fetch_ics_events(url, user=None, password=None, verify_ssl=True): + try: + response = requests.get(url, auth=(user, password) if user else None, verify=verify_ssl) + response.raise_for_status() + events = [] + ics_data = response.text + + if not ics_data.strip(): + logging.warning("Leere ICS-Daten empfangen") + return events + + try: + for calendar in vobject.readComponents(ics_data): + for component in calendar.components(): + if component.name == 'VEVENT': + if hasattr(component, 'uid') and component.uid.value: + events.append(component) + else: + logging.warning("VEVENT ohne UID übersprungen") + except vobject.base.ParseError as e: + logging.error(f"Parse-Fehler in ICS-Daten: {str(e)}") + return events + except Exception as e: + raise Exception(f"ICS-Fehler: {str(e)}") + +def connect_caldav(url, user=None, password=None, verify_ssl=True): + try: + client = DAVClient( + url, + username=user, + password=password, + ssl_verify_cert=verify_ssl + ) + # Direktes Calendar-Objekt, kein principal-Abgleich nötig + return Calendar(client=client, url=url) + except Exception as e: + raise Exception(f"CalDAV-Fehler: {str(e)}") + +def safe_caldav_event_processing(event): + try: + if event is None or not hasattr(event, 'data') or not event.data: + return None + for calendar in vobject.readComponents(event.data): + for component in calendar.components(): + if component.name == 'VEVENT' and hasattr(component, 'uid'): + return { + 'uid': str(component.uid), + 'event_object': event, + 'data': event.data + } + return None + except vobject.base.ParseError: + logging.warning("Parse-Fehler in CalDAV-Event, überspringe") + return None + except Exception as e: + logging.error(f"Fehler bei Event-Verarbeitung: {str(e)}") + return None + +def sync_ics_to_caldav(module): + ics_events = fetch_ics_events( + module.params['source_url'], + module.params['source_user'], + module.params['source_password'], + module.params['verify_ssl'] + ) + + target_cal = connect_caldav( + module.params['target_url'], + module.params['target_user'], + module.params['target_password'], + module.params['verify_ssl'] + ) + + existing_events = {} + for event in target_cal.events(): + processed = safe_caldav_event_processing(event) + if processed: + existing_events[processed['uid']] = processed + + changed = False + results = {'added': [], 'updated': [], 'removed': []} + + for vevent in ics_events: + uid = str(vevent.uid) + try: + ical_data = vevent.serialize() + except Exception as e: + logging.error(f"Serialisierungsfehler für UID {uid}: {str(e)}") + continue + + if not ical_data.strip(): + logging.warning(f"Leere iCal-Daten für UID {uid}, überspringe") + continue + + if uid not in existing_events: + try: + target_cal.add_event(ical_data) + results['added'].append(uid) + changed = True + except Exception as e: + logging.error(f"Hinzufügen fehlgeschlagen für UID {uid}: {str(e)}") + else: + existing_data = existing_events[uid]['data'] + if ical_data != existing_data: + try: + event_obj = existing_events[uid]['event_object'] + event_obj.data = ical_data + event_obj.save() + results['updated'].append(uid) + changed = True + except Exception as e: + logging.error(f"Aktualisieren fehlgeschlagen für UID {uid}: {str(e)}") + + if module.params['purge']: + current_uids = {str(e.uid) for e in ics_events} + for uid, event_info in existing_events.items(): + if uid not in current_uids: + try: + event_info['event_object'].delete() + results['removed'].append(uid) + changed = True + except Exception as e: + logging.error(f"Löschen fehlgeschlagen für UID {uid}: {str(e)}") + + return changed, results + +def run_module(): + module_args = dict( + source_url=dict(type='str', required=True), + source_user=dict(type='str', required=False, default=None), + source_password=dict(type='str', required=False, no_log=True, default=None), + 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', default=False), + purge=dict(type='bool', default=False) + ) + + module = AnsibleModule(argument_spec=module_args, supports_check_mode=False) + + try: + changed, results = sync_ics_to_caldav(module) + module.exit_json(changed=changed, **results) + except Exception as e: + module.fail_json(msg=str(e)) + +if __name__ == '__main__': + run_module()