src | ||
.gitignore | ||
LICENSE | ||
README.md |
Google Kontakte → Kalender: Geburtstage-Sync (People API)
Diese Anleitung ist End‑to‑End und für Einsteiger geeignet. Sie umfasst: Hintergrund (warum der alte Geburtstags‑Kalender nicht mehr geht), Einrichtung der People API (Advanced Service + Cloud Console), Script Properties für eine flexible Kalender‑ID, das fertige Skript (mit Alter im Titel), Trigger, Fehlerdiagnose und FAQ. Alle Schritte sind klickbar verlinkt.
Warum nicht mehr addressbook#contacts…
?
Der frühere Geburtstags‑Kalender war ein echter Kalender mit fester ID. Heute ist „Geburtstage“ oft nur noch eine Overlay‑Ansicht ohne ID → in Apps Script nicht mehr direkt nutzbar. Lösung: Geburtstage direkt aus Google Kontakte (People API) lesen und in einen echten Zielkalender schreiben.
Voraussetzungen
- Google Konto mit Google Kontakte und Google Kalender.
- Zugriff auf Apps Script: <https://script.google.com>
- (Empfohlen) Eigener Zielkalender, z. B. „Geburtstage (Sync)“.
Schritt 1 – Zielkalender anlegen & ID kopieren
- Öffne Google Kalender → Zahnrad → Einstellungen → + Kalender hinzufügen → Neuen Kalender erstellen (z. B. „Geburtstage (Sync)“).
- In den Kalendereinstellungen die Kalender‑ID kopieren (endet auf
@group.calendar.google.com
).
Diese ID speichern wir gleich als Script Property, damit der Code nicht hardcodiert ist.
Schritt 2 – Apps Script Projekt & Zeitzone
- Öffne <https://script.google.com> → Neues Projekt → Name z. B.
Birthdays Sync
. - Projekt‑Einstellungen (Zahnradsymbol) → Zeitzone auf Europe/Berlin stellen.
Schritt 3 – People API aktivieren (2 Schalter)
- Advanced Google services in Apps Script aktivieren:
Link: <https://developers.google.com/apps-script/guides/services/advanced>
Im Editor links auf Services (Puzzleteil) → Add a service → People API → Add. - People API im verknüpften Google Cloud‑Projekt aktivieren:
Link: <https://developers.google.com/people/v1/getting-started> → „use the setup tool“
Direkt zur Console: <https://console.cloud.google.com/apis/library/people.googleapis.com> → Enable.
(Allgemeine Anleitung: <https://support.google.com/googleapi/answer/6158841>)
Ohne beide Schritte ist
People.*
in Apps Script nicht verfügbar.
Schritt 4 – Script Property für die Kalender‑ID setzen
Script Properties sind Key/Value‑Paare fürs Projekt.
- Apps Script → Datei → Projekteigenschaften → Script Properties.
- Neu: Key
BIRTHDAY_CALENDAR_ID
→ Value = deine Kalender‑ID (z. B.…@group.calendar.google.com
) → Speichern.
Dokumentation:
- Guide: <https://developers.google.com/apps-script/guides/properties>
- API: <https://developers.google.com/apps-script/reference/properties/properties-service>
Schritt 5 – Fertiger Code (mit Alter im Titel + Properties‑Support)
Hinweis: Der Code löscht nur eigene Events (MARKER) im Fenster ±360 Tage und schreibt jeden Geburtstag für dieses & nächstes Jahr. Bei vorhandenem Geburtsjahr wird das Alter in Klammern angezeigt.
/**
* Synchronisiert Geburtstage aus Google Kontakte (People API) in einen Zielkalender.
* — Löscht zuvor nur eigene Script-Events (MARKER) im Fenster ±360 Tage.
* — Schreibt für aktuelles & nächstes Jahr; bei bekanntem Geburtsjahr mit Alter im Titel.
* — Zeitzone: Europe/Berlin (in Projekt-Einstellungen setzen!).
*
* Einrichtung:
* 1) Advanced Google service „People API“ aktivieren (Editor → Services → Add → People API).
* 2) People API im Google-Cloud-Projekt aktivieren (Console → APIs & Services → Library → People API → Enable).
* 3) Script Property setzen: BIRTHDAY_CALENDAR_ID = <deine @group.calendar.google.com-ID>.
*/
function runSync() {
var props = PropertiesService.getScriptProperties();
var calendarId = props.getProperty("BIRTHDAY_CALENDAR_ID");
if (!calendarId) throw new Error("Script Property BIRTHDAY_CALENDAR_ID ist nicht gesetzt.");
Sync_Birthdays_PeopleOnly_WithAge(calendarId, "SyncedBy=BirthdaysScript");
}
/**
* Kernfunktion: Liest Kontakte (People API) und erzeugt All-Day-Events im Zielkalender.
* @param {string} calendarId Zielkalender-ID (…@group.calendar.google.com)
* @param {string} [marker] Markertext zur Kennzeichnung Script-erzeugter Events
*/
function Sync_Birthdays_PeopleOnly_WithAge(calendarId, marker) {
marker = marker || "SyncedBy=BirthdaysScript";
var tz = Session.getScriptTimeZone() || "Europe/Berlin";
var dst = CalendarApp.getCalendarById(calendarId);
if (!dst) throw new Error("Zielkalender nicht gefunden: " + calendarId);
var today = new Date();
var startWin = atStartOfDay(addDays(today, -360), tz);
var endWin = atEndOfDay(addDays(today, 360), tz);
// 1) Eigene alten Einträge im Fenster entfernen
dst.getEvents(startWin, endWin).forEach(function(ev){
var d = ev.getDescription() || "";
if (d.indexOf(marker) !== -1) { try { ev.deleteEvent(); } catch(_){} }
});
// 2) Kontakte lesen & Geburtstage schreiben
var created = 0, pageToken = null;
do {
var resp = People.People.Connections.list("people/me", {
personFields: "names,birthdays,events",
sources: ["READ_SOURCE_TYPE_CONTACT"],
pageSize: 1000,
pageToken: pageToken
});
var conns = (resp && resp.connections) ? resp.connections : [];
conns.forEach(function(p){
var name = (p.names && p.names.length && p.names[0].displayName) ? p.names[0].displayName : null;
if (!name) return;
// Geburtstag aus birthdays oder events holen
var bd = pickBirthdayFromBirthdays(p.birthdays) || pickBirthdayFromEvents(p.events);
if (!bd || !bd.month || !bd.day) return; // nur echte Einträge (Monat+Tag)
// Immer aktuelles & nächstes Jahr
[today.getFullYear(), today.getFullYear() + 1].forEach(function(yr){
var day = (bd.month === 2 && bd.day === 29 && !isLeap(yr)) ? 28 : bd.day;
var when = new Date(yr, bd.month - 1, day);
if (when < startWin || when > endWin) return;
var title = "🎂 " + name;
if (bd.year) {
var age = yr - bd.year;
if (age >= 0 && age <= 150) title += " (" + age + ")";
}
try {
dst.createAllDayEvent(title, when, { description: marker });
created++;
} catch(_) {}
});
});
pageToken = resp && resp.nextPageToken ? resp.nextPageToken : null;
} while (pageToken);
console.log("Geburtstage erstellt:", created);
}
/**
* Wählt den primären Geburtstag aus dem Array `birthdays`; Fallback erster gültiger.
* @param {Array} arr People API birthdays-Array
* @returns {{year:(number|null),month:number,day:number}|null}
*/
function pickBirthdayFromBirthdays(arr) {
if (!arr || !arr.length) return null;
var primary = arr.find(function(b){ return b && b.metadata && b.metadata.primary && b.date; });
var any = primary || arr.find(function(b){ return b && b.date; });
if (!any || !any.date) return null;
return { year: any.date.year || null, month: any.date.month, day: any.date.day };
}
/**
* Extrahiert Geburtstag aus People API events (type=BIRTHDAY).
* @param {Array} arr People API events-Array
* @returns {{year:(number|null),month:number,day:number}|null}
*/
function pickBirthdayFromEvents(arr) {
if (!arr || !arr.length) return null;
var ev = arr.find(function(e){ return e && e.type && String(e.type).toUpperCase() === "BIRTHDAY" && e.date; });
if (!ev || !ev.date) return null;
return { year: ev.date.year || null, month: ev.date.month, day: ev.date.day };
}
/* === Hilfsfunktionen === */
/** Addiert Tage auf ein Datum. */
function addDays(date, days){ var d=new Date(date); d.setDate(d.getDate()+days); return d; }
/** Liefert Mitternacht (00:00:00) im angegebenen Zeitzonen-Format. */
function atStartOfDay(d, tz){ return new Date(Utilities.formatDate(d, tz, "yyyy-MM-dd'T'00:00:00")); }
/** Liefert 23:59:59.999 im angegebenen Zeitzonen-Format. */
function atEndOfDay(d, tz){ var dd=new Date(Utilities.formatDate(d, tz, "yyyy-MM-dd'T'23:59:59")); dd.setMilliseconds(999); return dd; }
/** Prüft, ob ein Jahr ein Schaltjahr ist. */
function isLeap(y){ return (y%4===0 && y%100!==0) || (y%400===0); }
Schritt 6 – Erster manueller Lauf (Scopes erteilen)
- Im Editor oben Funktion auswählen:
runSync
→ Ausführen. - Ersten Berechtigungsdialog bestätigen (People & Kalender).
- Danach im Zielkalender (F5) prüfen: neue 🎂‑Einträge sollten sichtbar sein.
Dokumentation:
- Advanced People Service (Apps Script): <https://developers.google.com/apps-script/advanced/people>
- People API Intro & Scopes: <https://developers.google.com/people>
people.connections.list
: <https://developers.google.com/people/api/rest/v1/people.connections/list>- CalendarApp Referenz: <https://developers.google.com/apps-script/reference/calendar/calendar-app>
Schritt 7 – Zeitgesteuerten Trigger einrichten
- Links Auslöser (Wecker‑Icon) → Auslöser hinzufügen.
- Funktion:
runSync
- Ereignisquelle: Zeitgesteuert → z. B. Täglich oder Stündlich → Speichern.
Trigger‑Guide: <https://developers.google.com/apps-script/guides/triggers>
Troubleshooting
**TypeError: People is undefined**
→ Advanced Service People API in Apps Script nicht aktiviert. (Schritt 3.1)**GoogleJsonResponseException: Access Not Configured**
→ People API in der Cloud Console nicht aktiviert. (Schritt 3.2)- 0 Einträge erstellt → In deinen Kontakten sind ggf. keine echten Geburtstage (Monat+Tag) gespeichert. Trage beim Kontakt im Feld „Geburtstag“ ein Datum ein.
- Keine Schreibrechte → Prüfe die Kalender‑ID und dass dein Konto Schreibrechte auf dem Zielkalender hat.
- 29. Februar → wird automatisch auf 28. Februar gesetzt, wenn kein Schaltjahr.
FAQ
Zeigt ihr Alter an, wenn kein Geburtsjahr vorhanden ist?
Nein. Ohne Geburtsjahr wird nur 🎂 Name
eingetragen.
Kann ich statt Script Property einen Parameter nutzen?
Ja: Sync_Birthdays_PeopleOnly_WithAge("<kalender-id>")
. Für Deployments über mehrere Umgebungen sind Script Properties komfortabler.
Wird der Zielkalender geleert?
Nur Events mit MARKER im ±360‑Tage‑Fenster werden entfernt – andere Einträge bleiben unangetastet.
Kann ich das Zeitfenster ändern?
Ja, im Code addDays(today, -360)
/ +360
anpassen (z. B. ±720 Tage).
Nützliche Links (Offizielle Doku)
- Advanced People Service (Apps Script): <https://developers.google.com/apps-script/advanced/people>
- People API – Getting started: <https://developers.google.com/people/v1/getting-started>
- People API –
people.connections.list
: <https://developers.google.com/people/api/rest/v1/people.connections/list> - Apps Script – Properties Service (Guide): <https://developers.google.com/apps-script/guides/properties>
- Apps Script – PropertiesService (API): <https://developers.google.com/apps-script/reference/properties/properties-service>
- Apps Script – CalendarApp Referenz: <https://developers.google.com/apps-script/reference/calendar/calendar-app>
- Apps Script – Trigger Leitfaden: <https://developers.google.com/apps-script/guides/triggers>
### Fertig.
Du kannst jetzt ohne Code‑Anpassung den Zielkalender per Script Property wechseln, regelmäßig per Trigger synchronisieren und bei vorhandenem Geburtsjahr wird automatisch das Alter angezeigt.
Beispiel: mehrere Kalender
/**
* Sync-Geburtstage aus Google Kontakte (People API) in mehrere Zielkalender.
*
* Script Property BIRTHDAY_CALENDAR_IDS = Komma-getrennte IDs:
* z.B.: "kal1@group.calendar.google.com,kal2@group.calendar.google.com"
*/
function runSyncMultiple() {
var props = PropertiesService.getScriptProperties();
var idsString = props.getProperty("BIRTHDAY_CALENDAR_IDS");
if (!idsString) throw new Error("Script Property BIRTHDAY_CALENDAR_IDS ist nicht gesetzt!");
var calendarIds = idsString.split(",").map(function(id){ return id.trim(); }).filter(Boolean);
if (!calendarIds.length) throw new Error("Keine gültigen Kalender-IDs gefunden.");
Sync_Birthdays_MultipleCalendars(calendarIds, "SyncedBy=BirthdaysScript");
}
/**
* Kernfunktion: liest Kontakte und schreibt Geburtstage in mehrere Kalender.
* @param {string[]} calendarIds Array von Kalender-IDs
* @param {string} marker Markertext für eigene Events
*/
function Sync_Birthdays_MultipleCalendars(calendarIds, marker) {
marker = marker || "SyncedBy=BirthdaysScript";
var tz = Session.getScriptTimeZone() || "Europe/Berlin";
var today = new Date();
var startWin = atStartOfDay(addDays(today, -360), tz);
var endWin = atEndOfDay(addDays(today, 360), tz);
// --- 1) Alle Zielkalender initialisieren ---
var dsts = calendarIds.map(function(id){
var c = CalendarApp.getCalendarById(id);
if (!c) throw new Error("Zielkalender nicht gefunden: " + id);
// Alte Script-Events löschen
c.getEvents(startWin, endWin).forEach(function(ev){
var d = ev.getDescription() || "";
if (d.indexOf(marker) !== -1) { try { ev.deleteEvent(); } catch(_){} }
});
return c;
});
// --- 2) Geburtstage aus Kontakte lesen (einmalig) ---
var birthdays = []; // Array von {name, month, day, year}
var pageToken = null;
do {
var resp = People.People.Connections.list("people/me", {
personFields: "names,birthdays,events",
sources: ["READ_SOURCE_TYPE_CONTACT"],
pageSize: 1000,
pageToken: pageToken
});
var conns = (resp && resp.connections) ? resp.connections : [];
conns.forEach(function(p){
var name = (p.names && p.names.length && p.names[0].displayName) ? p.names[0].displayName : null;
if (!name) return;
var bd = pickBirthdayFromBirthdays(p.birthdays) || pickBirthdayFromEvents(p.events);
if (!bd || !bd.month || !bd.day) return;
birthdays.push({name:name, month:bd.month, day:bd.day, year:bd.year || null});
});
pageToken = resp && resp.nextPageToken ? resp.nextPageToken : null;
} while (pageToken);
// --- 3) In alle Kalender schreiben ---
var createdTotal = 0;
birthdays.forEach(function(bd){
[today.getFullYear(), today.getFullYear() + 1].forEach(function(yr){
var day = (bd.month === 2 && bd.day === 29 && !isLeap(yr)) ? 28 : bd.day;
var when = new Date(yr, bd.month - 1, day);
if (when < startWin || when > endWin) return;
var title = "🎂 " + bd.name;
if (bd.year) {
var age = yr - bd.year;
if (age >= 0 && age <= 150) title += " (" + age + ")";
}
dsts.forEach(function(c){
try {
c.createAllDayEvent(title, when, { description: marker });
createdTotal++;
} catch(_) {}
});
});
});
console.log("Geburtstage erstellt in " + calendarIds.length + " Kalender(n):", createdTotal);
}
Einrichtung für mehrere Kalender
In den Script Properties statt BIRTHDAY_CALENDAR_ID nun setzen:
- Key: BIRTHDAY_CALENDAR_IDS
- Value: kal1@group.calendar.google.com,kal2@group.calendar.google.com,kal3@group.calendar.google.com
→ Komma-getrennt, beliebig viele Kalender.
- Trigger auf runSyncMultiple legen.