Skip to content

Vinix24/calsync

Repository files navigation

calsync

Tweerichtings-kalendersync tussen een persoonlijke Google-agenda (Gmail) en een Microsoft 365 (Outlook) werkagenda, met verschillende privacy per richting. Python, draait lokaal, geen externe sync-dienst.

Ik bouwde dit omdat ik mijn werkafspraken mét details in mijn privé-agenda wilde (zodat een meekijker thuis ze ziet), maar mijn privé-afspraken alleen als geblokkeerde tijd in mijn werkagenda, zonder details voor collega's.

Wat het doet

Twee aparte een-richtings-pipes, zodat de privacy per richting verschilt:

Pipe Transform Wat er overkomt
Outlook -> Gmail full_copy echte events met titel, body, locatie, tijd. Een meekijker ziet ze.
Gmail -> Outlook busy_block titel "Bezet", geen body, showAs=busy, private. Alleen geblokkeerde tijd.

Het worden echte gekloonde events (harde boekingen), geen overlay-view.

Architectuur

   ┌─────────────────────────────┐  pipe 1: full_copy (detail)  ┌─────────────────────────────┐
   │  Microsoft 365 / Outlook    │ ───────────────────────────▶ │  Google Calendar            │
   │  (werkagenda)               │                              │  (persoonlijk)              │
   │                             │ ◀─────────────────────────── │                             │
   │  collega's zien: "Bezet"    │  pipe 2: busy_block ("Bezet") │  meekijker ziet: volledig   │
   └──────────────┬──────────────┘                              └──────────────┬──────────────┘
                  │  Graph API                                      Calendar API │
                  └───────────────────────┐          ┌─────────────────────────┘
                                          ▼          ▼
                              ┌──────────────────────────────────┐
                              │   calsync  (host, elke 5 min)     │
                              │   leest beide, schrijft beide     │
                              └──────────────────────────────────┘

De twee clouds praten niet rechtstreeks met elkaar. calsync is het orkestrator-procesje ertussen dat beide pollt en gelijktrekt. Intern:

   cli.py  ─▶  engine.py (SyncEngine.run_pipe)  ─┬─▶  transforms.py  (full_copy | busy_block)
                                                 ├─▶  providers/     (graph_provider | google_provider)
                                                 └─▶  state.py       (SQLite: (pipe, src_id) -> dst_id + hash)

   per bron-event:  lees venster ▸ skip clones (origin-tag) ▸ de-dup ▸ transform ▸ create/update/delete ▸ map

Hoe het werkt

calsync/
  config.py              # paden, venster, markers (waarden via .env)
  state.py               # SQLite mapping-store (pipe, src_id) -> dst_id + hash
  transforms.py          # full_copy, busy_block
  engine.py              # SyncEngine.run_pipe: lees -> transform -> map -> schrijf/verwijder
  cli.py                 # --dry-run / --loop / --pipe
  providers/
    base.py              # CanonicalEvent (provider-neutraal event)
    graph_provider.py    # Microsoft Graph (MSAL token-cache, calendarView)
    google_provider.py   # Google Calendar (OAuth desktop, singleEvents)
  probe_graph_access.py  # eenmalige toegangstest Microsoft Graph
  data/                  # tokens + calsync.db (lokaal, niet committen)

Vijf ontwerpkeuzes die de moeilijke randen wegnemen:

  1. Terugkerende afspraken via instance-venster. calendarView (Graph) en events.list?singleEvents=true (Google) leveren losse instances binnen een rollend venster. Geen RRULE-vertaling, geen exception-gedoe.
  2. Loop-preventie via origin-tag. Elke clone krijgt een marker (extendedProperties.private in Google, een singleValueExtendedProperty in Graph). De engine slaat getagde events bij het lezen over, dus een clone wordt nooit teruggesynct.
  3. De-dup tegen dubbelingen. Bestaat een afspraak al in beide agenda's, dan maakt de engine geen tweede kopie: bij de detail-richting op gelijke titel+tijd, bij de Bezet-richting op tijdsoverlap.
  4. Mapping-state in SQLite. (pipe, bron_id) -> doel_id plus een content-hash om ongewijzigde events over te slaan en verwijderingen te vinden. Dunne laag, makkelijk te vervangen door een andere database-adapter.
  5. Verwijderingen via venster-diff. Een mapping waarvan het bron-event uit het venster verdween, betekent: clone weg.

Privacy en veiligheid

  • Alles draait lokaal. Tokens, credentials.json, .env en de SQLite-db staan in data//de projectmap en worden niet gecommit (zie .gitignore).
  • Alleen gedelegeerde rechten: de app werkt namens jou, op jouw eigen agenda. Geen toegang tot agenda's van anderen.
  • Er gaat geen data naar derden. Alleen verkeer naar de twee agenda-API's van Microsoft en Google, met jouw eigen app-registraties.

Clone en draaien

Setup-tijd: ~30-45 min. De volledige klik-voor-klik runbook (Entra-app, admin-consent, Google OAuth, valkuilen) staat in SETUP.md. Kort:

git clone <repo> calsync && cd calsync
python3 -m venv .venv && ./.venv/bin/pip install -r requirements.txt

cp .env.example .env          # vul je Entra client-id + tenant-id in
# plaats je Google credentials.json in deze map (zie SETUP.md, Deel B)

./run.sh --dry-run            # toon wat er zou gebeuren, schrijf niets
./run.sh                      # een keer syncen
./run.sh --loop 5             # blijf draaien, elke 5 minuten
./run.sh --pipe outlook->google   # alleen die richting

Gebruik ./run.sh (of .venv/bin/python cli.py), niet python3 cli.py: in sommige shells is python3 een alias die de venv negeert, met ModuleNotFoundError: msal als gevolg. De wrapper pakt altijd de venv-python.

Bij de eerste run vraagt Graph eenmalig een device-code-login en Google om browser-toestemming. Daarna draaien beide stil op bewaarde tokens.

Setup met een AI-assistent

Je hoeft geen developer te zijn. Clone de repo, open de map in een AI-coding-assistent (Claude, ChatGPT, of een CLI-variant) en plak deze prompt:

Ik wil de calsync-tool uit deze repo op mijn computer opzetten (ik gebruik macOS/Windows). Loop me stap voor stap door, stop bij elke stap en wacht op mij: (1) Python, een virtuele omgeving en de dependencies; (2) een Microsoft Entra app-registratie met gedelegeerd Calendars.ReadWrite + offline_access en device-code-login aan; (3) een Google Cloud OAuth desktop-client met credentials.json; (4) een .env met mijn client-id en tenant-id; (5) de eerste dry-run. Gebruik SETUP.md in deze repo als leidraad.

Twee dingen die een assistent niet voor je oplost: als je bedrijfstenant admin-consent vereist, heb je je IT-beheerder nodig (zie SETUP.md, Deel A-6). En de sync moet ergens blijven draaien, dus je hebt een computer of server nodig die aan blijft staan.

Continu laten draaien

./run.sh --loop 5 in een terminal of tmux, of een launchd-job (macOS) / cron (Linux). Voorbeeld-launchd-plist staat in SETUP.md, Deel E.

Vereisten

  • Python 3.10+
  • Een eigen Microsoft Entra app-registratie (gedelegeerd Calendars.ReadWrite + offline_access). Let op: veel bedrijfstenants vereisen admin-consent.
  • Een eigen Google Cloud OAuth desktop-client.

Licentie

MIT, zie LICENSE.

About

Two-way Outlook <-> Google Calendar sync with per-direction privacy (full detail one way, busy-only the other). Python, runs locally.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors