sharing/bday/item: add conversion function

This commit is contained in:
Peter Bieringer
2026-03-13 21:34:46 +01:00
parent 03b5ed162f
commit 4cc1f96ada

View File

@@ -26,6 +26,7 @@ Module for address books and calendar entries (see ``Item``).
import binascii import binascii
import contextlib import contextlib
import logging
import math import math
import os import os
import re import re
@@ -33,7 +34,7 @@ from datetime import datetime, timedelta
from hashlib import sha256 from hashlib import sha256
from itertools import chain from itertools import chain
from typing import (Any, Callable, List, MutableMapping, Optional, Sequence, from typing import (Any, Callable, List, MutableMapping, Optional, Sequence,
Tuple) Tuple, Union)
import vobject import vobject
@@ -42,6 +43,11 @@ from radicale import pathutils, utils
from radicale.item import filter as radicale_filter from radicale.item import filter as radicale_filter
from radicale.log import logger from radicale.log import logger
# Product ID for auto-conversion
PRODID_CONVERTED = u"-//Radicale//NONSGML " + utils.package_version("radicale") + "//EN (auto-converted)"
PRODID_SUFFIX = " (auto-converted by Radicale " + utils.package_version("radicale") + ")"
UID_SUFFIX = "-auto-converted-by-Radicale"
def read_components(s: str) -> List[vobject.base.Component]: def read_components(s: str) -> List[vobject.base.Component]:
"""Wrapper for vobject.readComponents""" """Wrapper for vobject.readComponents"""
@@ -498,3 +504,114 @@ class Item:
self.time_range self.time_range
self.component_name self.component_name
self._vobject_item = orig_vobject_item self._vobject_item = orig_vobject_item
def convert_vcf_to_ics(self) -> Union["Item" | None]:
if logger.isEnabledFor(logging.DEBUG):
logger.debug("TRACE/item/convert_vcf_to_ics: convert VCF to ICS (href): %r", self.href)
logger.debug("TRACE/item/convert_vcf_to_ics: convert VCF to ICS (vobject): %r", self.vobject_item)
if self.vobject_item.name != "VCARD":
if logger.isEnabledFor(logging.DEBUG):
logger.debug("TRACE/item/convert_vcf_to_ics: item is not a VCARD (skip): %r", self.href)
return None
else:
if logger.isEnabledFor(logging.DEBUG):
logger.debug("TRACE/item/convert_vcf_to_ics: item is a VCARD (ok): %r", self.href)
if not hasattr(self.vobject_item, "bday"):
if logger.isEnabledFor(logging.DEBUG):
logger.debug("TRACE/item/convert_vcf_to_ics: miss bday (skip): %r", self.href)
return None
else:
pass
bday = self.vobject_item.bday
if logger.isEnabledFor(logging.DEBUG):
logger.debug("TRACE/item/convert_vcf_to_ics: has bday (ok): %r -> %r", self.href, bday.value)
pattern = re.compile('^([0-9]{4})-?([0-9]{2})-?([0-9]{2})$')
match = pattern.match(bday.value)
if not match:
if logger.isEnabledFor(logging.DEBUG):
logger.debug("TRACE/item/convert_vcf_to_ics: has unsupported bday: %r -> %r", self.href, bday.value)
return None
else:
pass
bdayY = int(match[1])
bdayM = int(match[2])
bdayD = int(match[3])
# create ICS
if hasattr(self.vobject_item, "fn"):
name = self.vobject_item.fn.value
elif hasattr(self.vobject_item, "n"):
name = self.vobject_item.n.value
elif hasattr(self.vobject_item, "nickname"):
name = self.vobject_item.nickname.value
else:
if logger.isEnabledFor(logging.DEBUG):
logger.debug("TRACE/item/convert_vcf_to_ics: has bday but neither FN or N or NICKNAME (skip): %r", self.href)
return None
# create VCALENDAR
item_ics = vobject.newFromBehavior('vcalendar')
# set PRODID
if hasattr(self.vobject_item, "prodid"):
item_ics.add('prodid').value = self.vobject_item.prodid.value + PRODID_SUFFIX
else:
item_ics.add('prodid').value = PRODID_CONVERTED
# create EVENT
item_ics.add('vevent')
# set DTSTART
date = datetime(bdayY, bdayM, bdayD)
dtstart = date.strftime('%Y%m%d')
item_ics.vevent.add('dtstart;VALUE=DATE').value = dtstart
# calculate and set DTEND
date += timedelta(days=1)
item_ics.vevent.add('dtend;VALUE=DATE').value = date.strftime('%Y%m%d')
# set UID
if hasattr(self.vobject_item, "uid"):
pattern = re.compile('^(.*)-[0-9a-fA-F]{12}(.*)$')
match = pattern.match(self.vobject_item.uid.value)
if match:
# replace part of UUID by bday
item_ics.vevent.add('uid').value = match[1] + '-' + 'bda0' + dtstart + match[2]
else:
item_ics.vevent.add('uid').value = self.vobject_item.uid.value + UID_SUFFIX
else:
item_ics.vevent.add('uid').value = match[1] + match[2] + match[3] + "@" + name.replace(" ", "-") + UID_SUFFIX
# set SUMMARY
item_ics.vevent.add('summary').value = name + " (BDAY)"
# set RRULE
item_ics.vevent.add('rrule').value = "FREQ=YEARLY"
# add transparency
item_ics.vevent.add('transp').value = "TRANSPARENT"
href = self.href
if href is not None:
href = href.rstrip(".vcf") + ".ics"
etag = self.etag
# replace 14 leading chars of etag "<hexdigits>" by special format bda0YYYYMMDD00
etag = '"bda0' + dtstart + '00' + etag[15:]
item_new: Item = Item(
collection=self.collection,
etag=etag,
href=href,
vobject_item=item_ics)
if logger.isEnabledFor(logging.DEBUG):
logger.debug("TRACE/storage: item generated/vobject: %r", item_ics.serialize())
logger.debug("TRACE/storage: item orig /etag : %r", self.etag)
logger.debug("TRACE/storage: item generated/etag : %r", item_new.etag)
logger.debug("TRACE/storage: item generated/href : %r", item_new.href)
return item_new