#!/bin/bash # Google Calendar Birthday Sync # Fetches iCal feed, extracts birthdays, syncs with birthdays.json # Runs daily alongside birthday-tracker set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" source "$SCRIPT_DIR/../../.env" 2>/dev/null || true # Config ICAL_URL="${GOOGLE_CALENDAR_ICAL:-https://calendar.google.com/calendar/ical/anthonymau%40gmail.com/private-3636d9c51e7beda202676124684619ad/basic.ics}" BIRTHDAY_DATA="$SCRIPT_DIR/../birthday-tracker/birthdays.json" TELEGRAM_CHAT="${TELEGRAM_CHAT:-1793951355}" NTFY_URL="${NTFY_URL:-}" NTFY_TOPIC="${NTFY_TOPIC:-}" NTFY_MIN_PRIORITY="${NTFY_MIN_PRIORITY:-4}" TEMP_ICS="/tmp/gcal-birthdays.ics" log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" } # Fetch iCal feed fetch_ical() { log "Fetching Google Calendar iCal feed..." curl -s "$ICAL_URL" -o "$TEMP_ICS" || { log "ERROR: Failed to fetch iCal feed" return 1 } log "Fetched $(wc -l < "$TEMP_ICS") lines" } # Parse birthdays from iCal using Python parse_birthdays() { python3 << 'EOF' - "$TEMP_ICS" "$BIRTHDAY_DATA" import sys import json import re from datetime import datetime from icalendar import Calendar ics_path = sys.argv[1] data_path = sys.argv[2] # Load existing birthdays data with open(data_path, 'r') as f: data = json.load(f) existing_names = {p['name'].lower() for p in data['people']} new_birthdays = [] # Parse iCal with open(ics_path, 'rb') as f: cal = Calendar.from_ical(f.read()) for event in cal.walk('VEVENT'): summary = str(event.get('summary', '')) # Check if this looks like a birthday event if not re.search(r'\bbirthday\b', summary, re.IGNORECASE): continue # Extract name (remove "birthday" and common suffixes) name = re.sub(r'\s*(?:\'s|\s)?\s*birthday.*$', '', summary, flags=re.IGNORECASE).strip() name = re.sub(r'^(?:buy|get)\s+', '', name, flags=re.IGNORECASE).strip() name = name.replace("'s", "").strip() # Skip if already tracked or empty if not name or name.lower() in existing_names: continue # Get date dtstart = event.get('dtstart') if dtstart: dt = dtstart.dt if hasattr(dt, 'month') and hasattr(dt, 'day'): # Handle both date and datetime month = dt.month day = dt.day birth_year = dt.year if hasattr(dt, 'year') and dt.year > 1900 else None else: continue else: continue # Format birthday as MM-DD birthday = f"{month:02d}-{day:02d}" new_person = { "name": name, "relationship": "Contact (from Google Calendar)", "birthday": birthday, "source": "google-calendar", "gift_ideas": [], "past_gifts": [], "notes": f"Found in Google Calendar: {summary}" } if birth_year: new_person["birth_year"] = birth_year new_birthdays.append(new_person) print(f"Found: {name} - {birthday}", file=sys.stderr) # Output as JSON print(json.dumps(new_birthdays, indent=2)) EOF } # Merge new birthdays into existing data merge_birthdays() { local new_birthdays="$1" local count=$(echo "$new_birthdays" | jq 'length') if ((count == 0)); then log "No new birthdays found" return 0 fi log "Merging $count new birthdays..." # Add to existing data local temp_file=$(mktemp) jq --argjson new "$new_birthdays" '.people += $new' "$BIRTHDAY_DATA" > "$temp_file" mv "$temp_file" "$BIRTHDAY_DATA" log "Updated birthday database with $count new entries" # Send notification local names=$(echo "$new_birthdays" | jq -r '.[].name' | tr '\n' ', ' | sed 's/, $//') local message="📅 *Google Calendar Sync*\n\nFound and added *$count* new birthdays:\n$names\n\nThese will now be included in your birthday reminders!" # Telegram curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \ -H "Content-Type: application/json" \ -d "{\"chat_id\": \"$TELEGRAM_CHAT\", \"text\": \"$message\", \"parse_mode\": \"Markdown\"}" > /dev/null || true # ntfy if [[ -n "${NTFY_URL:-}" && -n "${NTFY_TOPIC:-}" ]]; then curl -s -X POST "${NTFY_URL%/}/${NTFY_TOPIC}" \ -H "Title: Birthday Sync" \ -H "Priority: ${NTFY_MIN_PRIORITY:-4}" \ -H "Sound: default" \ -d "Added $count new birthdays from Google Calendar: $names" > /dev/null 2>&1 || true fi } # Show current stats show_stats() { local total=$(jq '.people | length' "$BIRTHDAY_DATA") local gcal=$(jq '[.people[] | select(.source == "google-calendar")] | length' "$BIRTHDAY_DATA") local manual=$((total - gcal)) log "Birthday database stats:" log " Total: $total people" log " Manual entries: $manual" log " From Google Calendar: $gcal" } # Cleanup cleanup() { rm -f "$TEMP_ICS" } # Main main() { trap cleanup EXIT log "Starting Google Calendar birthday sync..." fetch_ical local new_birthdays new_birthdays=$(parse_birthdays) merge_birthdays "$new_birthdays" show_stats log "Sync complete!" } main "$@"