Come integrare l'API di iCloud Calendar nella tua app

Pubblicato il

In un articolo precedente abbiamo spiegato come integrare l'API di Google Calendar nella tua app e abbiamo ripercorso tutti i passaggi necessari affinché l'integrazione funzioni.

Integrare iCloud Calendar non è semplice come Google Calendar. La documentazione è scarsa e Apple non ha dedicato molto impegno a spiegare cosa deve fare uno sviluppatore per collegare iCloud Calendar a un’applicazione.

In questo articolo approfondiamo come integrare l’API di iCloud Calendar nella tua applicazione. Parliamo di autenticazione, operazioni supportate, limitazioni, frammenti di codice e strumenti che semplificano l’integrazione.

Quali protocolli e standard utilizza Apple iCloud Calendar?

Apple iCloud Calendar utilizza lo standard CalDAV per la comunicazione dei calendari. CalDAV è un’estensione di WebDAV che consente ai client di gestire calendari ed eventi su un server.

Gli eventi sono rappresentati nel formato ICS (iCalendar), un formato basato su testo per i dati di calendario. Ciò significa che la tua applicazione può comunicare con i calendari iCloud tramite HTTP utilizzando richieste CalDAV e dati ICS.

Il vantaggio è che non si tratta di un’implementazione specifica per i dispositivi o i sistemi operativi Apple; puoi utilizzarla da qualsiasi server che gira su qualunque piattaforma.

La cattiva notizia è che iCloud non fornisce un’API REST per i calendari, quindi CalDAV è l’unico modo per integrare iCloud Calendar nella tua app.

Come ci si autentica con iCloud?

Di solito, quando vuoi integrare una piattaforma nella tua applicazione (ad esempio Google Calendar), devi creare un account sviluppatore su quella piattaforma, creare un’applicazione, configurare gli scope, aggiungere utenti di test, compilare le informazioni dell’app e inviare per l’approvazione.

Dopo aver seguito tutti questi passaggi ed essere stato approvato, puoi quindi reindirizzare l’utente finale alla schermata OAuth della piattaforma, dove l’utente deve essere loggato e concedere l’accesso esplicito allo scope richiesto dalla tua applicazione. L’utente vede anche il nome della tua applicazione e tutte le informazioni che hai fornito quando hai configurato l’applicazione su quella piattaforma.

Apple iCloud non funziona in questo modo. iCloud non dispone di un flusso OAuth standard come Google Calendar o Outlook. Per collegare il calendario iCloud di un utente, ti autentichi con il suo Apple ID utilizzando l’Autenticazione di base (Basic Auth) su SSL. Poiché la maggior parte degli account iCloud ha attiva l’autenticazione a due fattori, l’utente deve creare una password specifica per l’app nelle impostazioni del proprio account Apple ID, invece di usare la password principale dell’account Apple.

La tua app chiederà all’utente il suo indirizzo email iCloud/Apple ID e questa password specifica per l’app di 16 caratteri. Usando queste credenziali, puoi connetterti al servizio CalDAV di iCloud.

Oltre alle ragioni elencate sopra, Apple richiede password specifiche per le app per l’accesso dei calendari di terze parti al fine di migliorare la sicurezza, poiché condividere la tua password iCloud con un’applicazione di terze parti non è l’ideale.

A livello tecnico, l’intestazione (header) della tua richiesta HTTP includerà la password specifica per l’app codificata in Base64, in questo formato: Authorization: Basic <app-specific-password-here>

Quali metodi supporta l’API di iCloud Calendar?

Il servizio CalDAV di iCloud è ospitato su caldav.icloud.com. Dopo l’autenticazione, è disponibile il seguente elenco di metodi:

  • Elencare i calendari di un utente
  • CRUD sugli eventi
  • Recuperare eventi specifici

Per saperne di più sullo standard CalDAV, leggi RFC 4791, che spiega tutti i metodi disponibili, i filtri e altro ancora.

Di seguito riepilogo le informazioni più importanti che devi conoscere per la tua integrazione con iCloud Calendar.

Verbi HTTP supportati:

Verbo HTTPCosa fa in CalDAVSupporto iCloudNote
OPTIONSScopre le capacità del serverUtile per il debug; non richiesto in produzione.
PROPFINDCerca principal, calendar-home-set, elenca calendari, ottiene proprietàÈ necessario autenticarsi prima; usare Depth 0 o 1.
MKCALENDARCrea una nuova raccolta di calendarioRichiede diritti di scrittura; vedi Sezione 5.3.1.
REPORTInterroga dati (calendar-query, calendar-multiget, free-busy-query)Tutti e tre i report sono obbligatori per le specifiche e presenti su iCloud.
PUTCarica / sostituisce una risorsa .ics (evento/attività)È necessario inviare l’intero VCALENDAR; niente PATCH.
DELETERimuove un evento o un calendarioDa usare insieme a If-Match ETag per sicurezza.
COPY / MOVECopia o sposta eventi tra calendariSoggetto alle stesse pre-condizioni di PUT.
GETRecupera una singola risorsa .icsRestituisce text/calendar ed ETag.

Proprietà delle collezioni calendario

ProprietàScopoNote specifiche di iCloud
CALDAV:calendar-descriptionDescrizione leggibilePienamente supportata
CALDAV:calendar-timezoneFuso orario predefinito per le querySupportato
CALDAV:supported-calendar-component-setQuali componenti (VEVENT, VTODO) il calendario accettaEventi e attività in calendari separati.
CALDAV:supported-calendar-dataMIME/versione consentita (di solito text/calendar 2.0)iCloud = default
CALDAV:max-resource-sizeDimensioni massime per eventoLimite iCloud ≈ 20 MB
CALDAV:min/max-date-time, max-instances, max-attendees-per-instanceVari limiti del serverRispettare per evitare errori 403/507.

Quali librerie puoi usare per semplificare l’integrazione?

Gestire CalDAV, ICS, XML e gli altri dettagli specifici di iCloud Calendar non è l’ideale. Spenderesti molto tempo a comprendere ogni metodo, convertire XML in JSON e inserirlo nel tuo codice.

Per un’integrazione più agevole, consigliamo le seguenti librerie:

  • tsdav : Imperdibile se utilizzi JavaScript/TypeScript. Con tsdav puoi comunicare facilmente con il server iCloud senza dover usare la sintassi o la terminologia specifica di CalDAV. tsdav fornisce un’API TypeScript di alto livello che incapsula tutti i verbi HTTP e l’XML che altrimenti scriveresti manualmente (PROPFIND, REPORT, MKCALENDAR, PUT, DELETE, ecc.). Consulta la documentazione di tsdav per saperne di più su come funziona.
  • ical-generator : Quando integri iCloud Calendar tramite CalDAV, devi caricare e sostituire interi file .ics ogni volta che crei o aggiorni un evento. Scrivere questi file a mano è soggetto a errori e ogni VEVENT richiede gli header corretti, UID, formattazione DTSTART/DTEND, stringhe RRULE, fusi orari e altro. ical-generator ti aiuta a gestire tutti questi aspetti.
  • ical.js è un parser/engine JavaScript puro creato dal team Mozilla Calendar, usato per analizzare le risposte ics in classi JS.

La tabella seguente spiega il ruolo di tsdav nell’integrazione con iCloud Calendar:

Ruolo nello stackCosa fa tsdavPerché è importante per iCloud
CalDAV / WebDAV clientFornisce un’API TypeScript di alto livello che incapsula tutti i verbi HTTP e l’XML che altrimenti dovresti scrivere e convertire manualmente.Ti permette di concentrarti sulla logica di business invece di generare stringhe XML raw e analizzare le risposte multistatus.
Helper di discoverycreateDAVClient() segue automaticamente il flusso di discovery CalDAV: contatta caldav.icloud.com, trova il principal dell’utente, risolve il calendar-home-set e memorizza l’URL base corretto pXX-caldav.icloud.com per le chiamate successive.Elimina il boilerplate per la danza di discovery in due fasi unica di iCloud.
Wrapper di autenticazioneHelper integrati per Basic e OAuth 2. Per iCloud passi { username: 'user@icloud.com', password: '<app-specific-pw>', authMethod: 'Basic' }.Non serve codificare le credenziali in Base64 o inserire gli header manualmente.
Helper tipizzati per attività comunifetchCalendars(), fetchCalendarObjects(), createCalendarObject(), updateCalendarObject(), deleteCalendarObject() restituiscono/accettano oggetti JS plain invece di XML.Implementi rapidamente il CRUD senza preoccuparti della sintassi XML di RFC-4791.
Supporto token di syncsyncCollection() incapsula il REPORT CalDAV sync-collection, traccia i token e restituisce solo gli elementi modificati/eliminati.Ti consente di implementare il polling per iCloud (che non ha push) con una sola riga.
Compatibilità Browser + NodeFunziona nel codice server (Node) o nel browser grazie all’uso di fetch isomorfico.Utile se parte della tua app gira in un’estensione browser o SPA.
Progetto TS moderno e tipizzatoViene fornito con type def completi, moduli ES tree-shakable e dipendenze minime.Facile da integrare nelle pipeline di build moderne.

Esempio di integrazione di iCloud Calendar usando tsdav + ts

In questo esempio si presume che tu abbia già ottenuto lo username dell’utente e la password specifica per l’app di Apple.

createClient Metodo che crea il DAVClient:

import { DAVCalendar, DAVClient, DAVNamespaceShort, DAVObject } from "tsdav";

const APPLE_DAV_URL = "https://caldav.icloud.com";

function createClient({
  username,
  password,
}: {
  username: string;
  password: string;
}) {
  const client = new DAVClient({
    serverUrl: APPLE_DAV_URL,
    credentials: {
      username,
      password,
    },
    authMethod: "Basic",
    defaultAccountType: "caldav",
  });

  return client;
}

constants File che contiene costanti

/*
The prodId value is a required field that must appear on every calendar object.
The field is a globally-unique identifier for the software that 
produced the file. tsdav might automatically generate that information for you,
but it might be best if you provide it manually.

You might find it useful when you have edge cases, you can use it to tell which
program wrote the data.

*/

export const PROD_ID = {
  company: "your-company-name-here",
  product: "your-product-name-here",
};

getCalendars Metodo che recupera tutti i calendari

  async getCalendars(
    ...params: Parameters<typeof DAVClient.prototype.fetchCalendars>
  ): Promise<DAVCalendar[]> {
  
  // You can abstract this initialization into another method.
  // For the sake of simplicity, we'll initialize the client on each method.
   const client = createClient({
    username: <email-here>,
    password: <password-here>,
  });
  
    return client.fetchCalendars(...params);
  }

getCalendarById Metodo che recupera un calendario per ID

  async getCalendarById(calendarUrl: string): Promise<DAVCalendar> {
    const calendars = await getCalendars();
    const calendar = calendars.find((el) => el.url === calendarUrl);
    if (!calendar) {
      throw new Error(`Apple Calendar with id ${calendarUrl} not found`);
    }

    return calendar;
  }

getCalendarEvents Metodo per elencare tutti gli eventi del calendario

  async getCalendarEvents(
    calendarUrl: string,
    query: GetCalendarEventsQuery = {}
  ) {
    const client = createClient({
      username: <email-here>,
      password: <password-here>,
    });

    const calendar = await getCalendarById(calendarUrl);

    const events = await client.fetchCalendarObjects({
        calendar,
        timeRange: query.dateRange ?? undefined,
      })

    return { events, nextSyncToken: calendar.syncToken };
  }

getEventById Metodo che restituisce un evento del calendario per ID

  async getEventById(calendarUrl: string, eventId: string) {
    const client = createClient({
      username: <email-here>,
      password: <password-here>,
    });
    
    const eventUrl = new URL(`${eventId}.ics`, calendarUrl).pathname;

    const responses = await client.calendarMultiGet({
        url: calendarUrl,
        props: {
          [`${DAVNamespaceShort.DAV}:getetag`]: {},
          [`${DAVNamespaceShort.CALDAV}:calendar-data`]: {},
        },
        objectUrls: [eventUrl],
        depth: "1",
      })
    

    if (responses.length === 0) {
      throw new Error(
        `Received no response while fetching ${eventUrl}`,
        null
      );
    } else if (responses[0].status >= 400) {
      throw new Error(
        `Failed to get Apple event by id. Status: ${responses[0].statusText}`,
        responses[0]
      );
    }

    const response = responses[0];
    const calendarObject: DAVObject = {
      url: new URL(response.href ?? "", calendarUrl).href,
      etag: `${response.props?.getetag}`,
      data:
        response.props?.calendarData?._cdata ?? response.props?.calendarData,
    };

    try {
      return calendarObject;
    } catch (err: any) {
      this.logger.error("Failed to process Apple Response", {
        message: err.message,
        event: calendarObject,
      });
      return [];
    }
  }

createEvent Metodo che crea un evento

  async createEvent(calendarUrl: string, data: AppleEvent) {
    const client = createClient({
      username: <email-here>,
      password: <password-here>,
    });

    const eventId = data.id ?? <generate-id-here>

    const calendar = ical({
	    prodId: PROD_ID,
      method: ICalCalendarMethod.REQUEST,
    });
    
    const event = calendar.createEvent({ ...data, id: eventId });

    const response = await  client.createCalendarObject({
        calendar: {
          url: calendarUrl,
        },
        filename: `${event.id()}.ics`,
        iCalString: calendar.toString(),
      })
      
    if (!response.ok) {
      throw new Error(
        `Failed to create Apple event: ${response.statusText}`,
        response
      );
    }
    
    return { id: event.id(), eventWithExceptions };
  }

updateEvent Metodo che aggiorna un evento

  async updateEvent(calendarUrl: string, eventId: string, data: AppleEvent) {
    const client = createClient({
      username: <email-here>,
      password: <password-here>,
    });

		
    const originalEventData = await getEventById(calendarUrl, eventId);

    const calendar = ical({
      prodId: PROD_ID,
      method: ICalCalendarMethod.REQUEST,
    });

    for (let event of originalEventData) {
      if (event.status === ICalEventStatus.CANCELLED) continue;

      if (event.id === eventId) {
        calendar.createEvent({ ...event, ...data, id: eventId, url: null });
      } else {
        calendar.createEvent({ ...event, id: eventId, url: null });
      }
    }

    const calendarObjectUrl = new URL(`${eventId}.ics`, calendarUrl);

    const response = await
      client.updateCalendarObject({
        calendarObject: {
          url: calendarObjectUrl.href,
          data: calendar.toString(),
        },
      })

    if (!response.ok) {
      throw new Error(
        `Failed to update Apple event: ${response.statusText}`,
        response
      );
    }

    return getEventById(calendarUrl, eventId);
  }

deleteEvent Metodo che elimina un evento per ID

  async deleteEvent(calendarUrl: string, eventId: string) {
    const client = createClient({
      username: <email-here>,
      password: <password-here>,
    });
    

    const calendarObjectUrl = new URL(`${eventId}.ics`, calendarUrl);

    const response = await client.deleteCalendarObject({
        calendarObject: {
          url: calendarObjectUrl.href,
        },
      })

    if (!response.ok) {
      throw new Error(
        `Failed to delete Apple event: ${response.statusText}`,
        response
      );
    }

    return { id: eventId };
  }

Quali limitazioni ha Apple iCloud Calendar?

  1. Nessuna API REST né risposte JSON: Per comunicare con l’API di iCloud Calendar devi usare CalDAV e lavorare con file .ics. Non è l’ideale, perché la Outlook Calendar API e la Google Calendar API hanno API REST che lavorano con JSON e sono molto più produttive. Puoi usare le librerie che ho suggerito per semplificare la comunicazione con l’API di iCloud Calendar, ma alla fine dipende dal linguaggio e dal framework che usi, perché non tutti i framework dispongono di librerie che facilitano la comunicazione con l’API di iCloud Calendar.
  2. Documentazione carente : Anche se abbiamo linkato la documentazione ufficiale di CalDAV, devi sapere che non tutti i metodi funzionano quando comunichi con l’API di iCloud Calendar, quindi preparati a sorprese e a molti test.
  3. Basic Auth con password specifica per l’app : Come menzionato, iCloud non segue le convenzioni standard OAuth 2.0; devi usare una password specifica per l’app per autenticarti e comunicare con iCloud Calendar.
  4. Nessun supporto per webhook/notifiche push : A differenza di Google Calendar o Outlook, iCloud non dispone della migliore API di calendario , poiché non puoi registrare webhook per essere notificato delle modifiche nei calendari. Le app di terze parti non possono iscriversi a aggiornamenti in tempo reale. Un workaround è effettuare polling periodico e utilizzare il report sync-collection per ottenere in modo efficiente le modifiche.
  5. Nessun supporto per i metodi PATCH : Non puoi usare i metodi PATCH per aggiornare parzialmente gli eventi; devi invece eseguire un PUT completo per aggiornare gli eventi.
  6. Nessun controllo sugli inviti : iCloud Calendar gestisce automaticamente gli inviti alle riunioni. Se crei o modifichi un evento con partecipanti, iCloud Calendar invierà gli inviti e aggiornerà gli stati dei partecipanti. Non puoi usare la pianificazione Outbox/Inbox CalDAV per controllare manualmente gli inviti.

Esiste un modo più semplice per integrare iCloud Calendar nella mia applicazione?

Integrare iCloud Calendar nella tua applicazione non è un’impresa da poco. iCloud non segue le convenzioni standard dei calendari, e molti metodi e filtri CalDAV che dovrebbero funzionare semplicemente non funzionano.

Un modo più semplice per integrare iCloud Calendar nella tua applicazione è usare un’API di calendario unificata (Unified Calendar API).

Illustrazione della Apiroc Unified Calendar API

Usare una Unified Calendar API come Apiroc offre i seguenti vantaggi:

  • Integra iCloud Calendar nella tua applicazione tramite un’API ben documentata e testata che segue standard moderni. Con Apiroc, un account iCloud si collega con l’e-mail dell’Apple ID e una password specifica per l’app tramite una sola chiamata API, e gli eventi vengono restituiti in JSON invece che in ICS.
  • Trascorri meno tempo nello sviluppo e nella manutenzione dell’integrazione. La maggior parte del lavoro è già fatta: l’API, il client e i casi limite. Inoltre, non dovrai mantenere l’integrazione o correggere i nuovi casi limite che si presentano nel tempo.
  • Integra altri provider di calendario oltre a iCloud Calendar senza lavoro aggiuntivo. Apiroc supporta Google Calendar e Microsoft Outlook tramite la stessa API, quindi aggiungere altri provider alla tua applicazione è facile.
  • Webhook per Google Calendar e Outlook, con supporto per iCloud in programma. Apiroc invia già oggi notifiche in tempo reale per i calendari Google e Microsoft. I webhook per i calendari iCloud sono in programma. Fino ad allora devi ancora fare polling per le modifiche di iCloud, cosa che Apiroc semplifica con i token di sincronizzazione. Apiroc Unified Calendar API - Supporto per i webhook

Usa Apiroc per integrare iCloud Calendar nella tua applicazione

Non devi occuparti da solo di tutte le complessità legate all’integrazione di iCloud Calendar nella tua applicazione. Utilizza invece Apiroc, una Unified Calendar API creata da un team con anni di esperienza nelle integrazioni con i calendari. Il piano gratuito include fino a 10 End User Account e richieste API illimitate. Puoi registrarti gratuitamente, senza bisogno di carta di credito.

Domande frequenti

Quale protocollo utilizza iCloud Calendar?

Utilizza CalDAV su HTTP e salva gli eventi nel formato iCalendar (ICS).

iCloud Calendar offre una REST API?

No. CalDAV è l’unico modo per leggere e scrivere i dati di iCloud Calendar.

Come si effettua l’autenticazione a iCloud Calendar?

Si passa l’ID Apple dell’utente (di solito l’e-mail) e una password specifica per l’app di 16 caratteri tramite Basic Auth su SSL.

iCloud Calendar supporta notifiche push o webhook?

No. È necessario effettuare polling per rilevare le modifiche.

Quali librerie semplificano l’integrazione?

tsdav (client CalDAV), ical-generator (creazione di ICS) e ical.js (parsing di ICS) gestiscono la maggior parte dei dettagli di basso livello.

Come evitare di trattare direttamente con CalDAV e ICS?

Usa una Unified Calendar API come Apiroc, che integra iCloud, Google e Outlook tramite un’unica interfaccia JSON moderna.