"""Pipeline: anade UN contacto a la libreta CardDAV de Enmanuel en una llamada. Compone funciones del registry: genera un UID determinista cuando el caller no da uno (contact_import_key) para que re-anadir el mismo contacto sobrescriba en vez de duplicar, serializa el dict de contacto a VCARD 3.0 (build_vcard), resuelve la contrasena CardDAV desde `pass` (pass_get_secret) y sube el VCARD via HTTP PUT (carddav_put_vcard). Impuro (red + lectura de `pass`). Solo stdlib. La contrasena resuelta NUNCA se logea ni se incluye en el dict de retorno. """ import os import sys sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) from core.build_vcard import build_vcard from core.contact_import_key import contact_import_key from infra.carddav_put_vcard import carddav_put_vcard from infra.pass_get_secret import pass_get_secret # Config destino embebida (libreta CardDAV de Enmanuel en Xandikos self-hosted). DEFAULT_BASE_URL = "https://dav-eedeb681c4ab89ab8e444ac9.organic-machine.com" DEFAULT_USERNAME = "enmanuel" DEFAULT_COLLECTION = "/enmanuel/contacts/addressbook/" def _as_list(value) -> list: """Normaliza None / string suelto / lista a lista de strings. None -> []; string suelto -> [string]; lista|tupla -> lista. Cualquier otro valor escalar se envuelve en una lista de un elemento. """ if value is None: return [] if isinstance(value, str): return [value] if isinstance(value, (list, tuple)): return list(value) return [value] def add_contact_dav( name: str, *, tels=None, emails=None, adrs=None, org: str = "", note: str = "", uid: str = "", base_url: str = DEFAULT_BASE_URL, username: str = DEFAULT_USERNAME, collection_path: str = DEFAULT_COLLECTION, secret_path: str = "dav/xandikos-enmanuel", timeout_s: float = 20.0, verify_tls: bool = True, ) -> dict: """Anade un contacto a la libreta CardDAV en una sola llamada (one-shot). Args: name: nombre completo del contacto (FN del vCard). Obligatorio. tels: telefono(s). Acepta lista, string suelto o None. emails: email(s). Acepta lista, string suelto o None. adrs: direccion(es). Acepta lista, string suelto o None. org: organizacion (ORG). Vacio = se omite. note: nota libre (NOTE). Vacio = se omite. uid: UID del vCard. Si se deja vacio se calcula con contact_import_key (telefono > email > nombre), de modo que re-anadir el mismo contacto sobrescribe el recurso en vez de duplicarlo (idempotencia). base_url: URL base del servidor DAV. Default = libreta de Enmanuel. username: usuario HTTP Basic. Default = enmanuel. collection_path: ruta de la coleccion CardDAV destino. secret_path: ruta del secreto en `pass` con la contrasena (primera linea). timeout_s: timeout del PUT en segundos. Default 20.0. verify_tls: si True (default) verifica el certificado TLS. Returns: dict. En exito reusa el dict de carddav_put_vcard mas el uid usado: {status:'ok', http_status:int, url:str, uid:str}. En error (sin lanzar): {status:'error', error:str, uid:str, http_status:int|None}. Si la contrasena no se encuentra en `pass`, devuelve {status:'error', error:..., uid:...} sin tocar la red. """ tels_list = _as_list(tels) emails_list = _as_list(emails) adrs_list = _as_list(adrs) used_uid = uid.strip() if uid else "" if not used_uid: used_uid = contact_import_key(name, phones=tels_list, emails=emails_list) contact = {"uid": used_uid, "fn": name} if tels_list: contact["tels"] = tels_list if emails_list: contact["emails"] = emails_list if adrs_list: contact["adrs"] = adrs_list if org: contact["org"] = org if note: contact["note"] = note vcard_text = build_vcard(contact) secret = pass_get_secret(secret_path) if secret.get("status") != "ok": return { "status": "error", "error": "pass: %s" % secret.get("error", "secret not found"), "uid": used_uid, "http_status": None, } password = secret["value"] result = carddav_put_vcard( base_url, username, password, collection_path, used_uid, vcard_text, timeout_s=timeout_s, verify_tls=verify_tls, ) # Reusar el dict de carddav_put_vcard + asegurar el uid usado. result["uid"] = used_uid return result if __name__ == "__main__": import argparse import json parser = argparse.ArgumentParser( description="Anade UN contacto a la libreta CardDAV de Enmanuel." ) parser.add_argument("--name", required=True, help="Nombre completo (FN).") parser.add_argument( "--tel", action="append", default=[], help="Telefono (repetible)." ) parser.add_argument( "--email", action="append", default=[], help="Email (repetible)." ) parser.add_argument( "--adr", action="append", default=[], help="Direccion (repetible)." ) parser.add_argument("--org", default="", help="Organizacion (ORG).") parser.add_argument("--note", default="", help="Nota libre (NOTE).") parser.add_argument("--uid", default="", help="UID explicito (opcional).") parser.add_argument("--base-url", default=DEFAULT_BASE_URL) parser.add_argument("--username", default=DEFAULT_USERNAME) parser.add_argument("--collection-path", default=DEFAULT_COLLECTION) parser.add_argument("--secret-path", default="dav/xandikos-enmanuel") parser.add_argument("--timeout-s", type=float, default=20.0) parser.add_argument( "--no-verify-tls", action="store_true", help="Desactiva la verificacion TLS (solo pruebas).", ) args = parser.parse_args() out = add_contact_dav( args.name, tels=args.tel, emails=args.email, adrs=args.adr, org=args.org, note=args.note, uid=args.uid, base_url=args.base_url, username=args.username, collection_path=args.collection_path, secret_path=args.secret_path, timeout_s=args.timeout_s, verify_tls=not args.no_verify_tls, ) print(json.dumps(out, ensure_ascii=False))