Skip to content

weorbitant/orbitant-calendar-sync

Repository files navigation

Orbitant Calendar Sync

Multi-tenant calendar aggregation service that synchronizes events from Google Calendar, Microsoft Outlook, and iCal feeds — with Slack bot integration and unified iCal feed output.

Features

  • Multi-provider sync — Google Calendar, Microsoft Outlook, and remote iCal feeds
  • Slack integration/ajustes (settings) and /calendario (view events) commands
  • Unified iCal feeds — Subscribe from any calendar app via a personal feed URL
  • Automatic sync — Configurable cron-based synchronization (default: every 15 minutes)
  • Per-user OAuth 2.0 — Each Slack user authenticates independently with calendar providers
  • Encrypted token storage — AES-256-GCM with PBKDF2 key derivation

Tech Stack

  • Runtime: Node.js 22
  • Server: Express
  • Database: SQLite via better-sqlite3
  • Slack: Slack Bolt (Socket Mode)
  • Google API: googleapis
  • Microsoft API: @azure/msal-node, @microsoft/microsoft-graph-client
  • Calendar parsing: ical.js
  • Date/time: Luxon
  • Scheduling: cron
  • Linting: ESLint 9 (flat config)

Prerequisites

  • Node.js 22+ (see .nvmrc)
  • Google Cloud OAuth 2.0 credentials
  • Azure app registration (for Microsoft/Outlook)
  • Slack App with Socket Mode enabled

Installation

# Clone repository
git clone <repo-url>
cd orbitant-calendar-sync

# Install dependencies
npm install

# Configure environment variables
cp .env.example .env
# Edit .env with your credentials

# Start server
npm start

Configuration

Edit the .env file with the following variables (see .env.example for reference):

Google OAuth

Variable Description
GOOGLE_CLIENT_ID OAuth 2.0 client ID from Google Cloud Console
GOOGLE_CLIENT_SECRET OAuth 2.0 client secret
GOOGLE_REDIRECT_URI Redirect URI (e.g., http://localhost:3000/auth/google/callback)
GOOGLE_SCOPES Comma-separated scopes (calendar.readonly, userinfo.email)
GOOGLE_CALENDAR_ID Calendar ID to sync (default: primary)

Microsoft/Azure OAuth

Variable Description
AZURE_CLIENT_ID Application (client) ID from Azure Portal > App registrations
AZURE_CLIENT_SECRET Client secret value
AZURE_TENANT_ID Directory (tenant) ID — use common for multi-tenant
AZURE_REDIRECT_URI Redirect URI (e.g., http://localhost:3000/auth/azure/callback)
AZURE_SCOPES Comma-separated Microsoft Graph scopes

Slack

Variable Description
SLACK_APP_TOKEN App-level token (xapp-...) with connections:write scope
SLACK_BOT_TOKEN Bot user OAuth token (xoxb-...)
SLACK_ADMINS Comma-separated Slack user IDs with admin privileges
SLACK_CHANNEL_ID Channel ID for notifications (optional)

Server & Database

Variable Description
PORT Server port (default: 3000)
BASE_URL Public URL used to generate iCal feed links
DATABASE_PATH SQLite database location (default: ./data/calendar.db)
TOKEN_ENCRYPTION_KEY 64-char hex key for encrypting OAuth tokens (required)

Generate the encryption key with:

node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"

Sync

Variable Description
SYNC_CRON Cron expression for auto-sync (default: 0 */15 * * * * — every 15 min)
SYNC_ON_STARTUP Whether to sync on server start (default: true)
MAINTENANCE_MODE Set to true to pause all sync jobs and gate Slack commands

Usage

Slack Commands

Command Description
/ajustes Manage connected accounts, calendar sources, and feed URL
/calendario View today's and tomorrow's events

HTTP Endpoints

Endpoint Description
GET /health Service health check
GET /feed/:token/orbitando.ics Unified iCal feed for a user
GET /auth/google/callback Google OAuth callback
GET /auth/azure/callback Microsoft OAuth callback

iCal Feed

After connecting your accounts via /ajustes, get your personal iCal feed URL and subscribe from:

  • Google Calendar
  • Apple Calendar
  • Microsoft Outlook
  • Any iCal-compatible client

Project Structure

src/
├── index.js                          # Express server, routes, startup
├── config/
│   └── database.js                   # SQLite initialization and migrations
├── database/
│   └── schema.sql                    # Database schema
├── jobs/
│   └── SyncScheduler.js             # Cron-based sync scheduling
├── models/
│   ├── Event.js                      # Calendar events
│   ├── FeedToken.js                  # iCal feed tokens per user
│   ├── OAuthToken.js                 # Encrypted OAuth tokens
│   ├── Source.js                     # Calendar sources
│   └── SyncState.js                  # Sync status per source
├── providers/
│   ├── BaseProvider.js               # Abstract base class
│   ├── GoogleCalendarProvider.js     # Google Calendar API v3
│   ├── MicrosoftCalendarProvider.js  # Microsoft Graph API
│   ├── ICalRemoteProvider.js         # Remote .ics URL fetching
│   └── ICalLocalProvider.js          # Local .ics file reading
├── scripts/
│   └── generate-tokens.js           # Legacy token script (deprecated)
├── services/
│   ├── CalendarAggregator.js         # Provider factory and cache
│   ├── google-calendar.js            # Google Calendar API service
│   ├── microsoft-calendar.js         # Microsoft Graph API service
│   ├── ICalGenerator.js              # iCal feed generation
│   └── SyncService.js                # Sync orchestration (singleton)
├── slack/
│   ├── app.js                        # Slack Bolt app setup
│   ├── actions/
│   │   ├── oauth.js                  # Google OAuth flow helpers
│   │   ├── microsoft-oauth.js        # Microsoft OAuth flow helpers
│   │   └── sources.js                # Source management actions
│   ├── commands/
│   │   ├── ajustes.js                # /ajustes command
│   │   └── calendario.js             # /calendario command
│   ├── middleware/
│   │   └── maintenanceMiddleware.js  # Maintenance mode gating
│   └── modals/
│       └── sourceModal.js            # Add/edit iCal source modal
└── utils/
    ├── crypto.js                     # AES-256-GCM encryption with PBKDF2
    ├── eventNormalizer.js            # Date/event normalization utilities
    ├── maintenance.js                # Maintenance mode management
    └── timezone.js                   # Timezone utilities

Architecture

Provider Pattern

All calendar sources extend BaseProvider, which defines the interface:

  • initialize() — Authenticate and connect to the calendar API
  • fetchEvents(options) — Full event fetch
  • sync(syncState) — Incremental sync (returns events, deletions, new sync state)
  • normalizeEvent(rawEvent) — Convert provider-specific format to unified schema
  • supportsIncrementalSync() — Whether provider supports delta sync

Implementations:

  • GoogleCalendarProvider — Google Calendar API v3, sync tokens for incremental updates
  • MicrosoftCalendarProvider — Microsoft Graph API, delta queries
  • ICalRemoteProvider — Fetches remote .ics URLs, uses ETags for caching
  • ICalLocalProvider — Reads local .ics files, uses mtime for change detection

Services

  • SyncService (singleton) — Orchestrates syncAll(), syncSource(id), syncUserSources(slackUserId)
  • CalendarAggregator — Factory that creates and caches provider instances per source
  • GoogleCalendarService — Google Calendar API wrapper with OAuth token management and auto-refresh
  • MicrosoftCalendarService — Microsoft Graph API wrapper with MSAL token management
  • ICalGenerator — Generates iCalendar output from stored events

Models

All models use better-sqlite3 directly (no ORM), with static methods (Active Record style):

Model Table Purpose
Source sources Calendar sources per user (google, microsoft, ical_remote, ical_local)
Event events Unified calendar events with source_id FK
OAuthToken oauth_tokens Encrypted OAuth tokens per Slack user + provider
SyncState sync_state Sync tokens, ETags, error states per source
FeedToken feed_tokens Unique iCal feed URL tokens per user

Slack Layer

  • Commands: /ajustes (settings UI), /calendario (event viewer)
  • Actions: OAuth flow initiation, source CRUD (add, edit, toggle, delete)
  • Modals: Source add/edit modal with name, type, URL, and color fields
  • Middleware: Maintenance mode gating for commands and actions

Database

SQLite schema (5 tables):

Table Key Constraints
sources Type CHECK (google, microsoft, ical_remote, ical_local), indexed on slack_user_id
events FK → sources (CASCADE), UNIQUE on (source_id, external_id)
sync_state FK → sources (CASCADE), UNIQUE on source_id
oauth_tokens UNIQUE on (slack_user_id, provider), provider CHECK (google, microsoft)
feed_tokens UNIQUE on both slack_user_id and token

The database is automatically initialized on startup from src/database/schema.sql. Migrations run in src/config/database.js.

Security

Token Encryption

OAuth tokens are encrypted before storage using AES-256-GCM with PBKDF2 key derivation:

  • Algorithm: AES-256-GCM (authenticated encryption)
  • Key derivation: PBKDF2 with SHA-512, 100,000 iterations, unique 64-byte salt per token
  • Storage format: salt:iv:authTag:ciphertext (hex-encoded)
  • Master key: 32-byte hex string from TOKEN_ENCRYPTION_KEY env var

Access Control

  • All source operations are scoped to the authenticated Slack user
  • User ownership is validated before source modifications
  • iCal feeds use unique, unguessable tokens per user
  • Admin actions (manual sync) restricted to SLACK_ADMINS user IDs

Docker

# Build and run
docker-compose up -d

# View logs
docker-compose logs -f

# Stop
docker-compose down

The container maps port 3030 (host) → 3000 (container). SQLite data is persisted via the ./data volume mount.

The Docker image is based on node:22 (Debian) with libsqlite3-dev for native SQLite bindings.

Maintenance Mode

Maintenance mode pauses all sync jobs and can gate Slack commands:

  • Enable via env var: Set MAINTENANCE_MODE=true (cannot be toggled at runtime)
  • Enable via flag file: Creates /tmp/maintenance.flag (can be toggled at runtime)
  • Priority: Environment variable takes precedence over flag file
  • Admin access: Admins listed in SLACK_ADMINS retain access during maintenance

Development

# Start with auto-reload (Node 22 --watch mode)
npm run dev

# Lint
npm run lint
npm run lint:fix

# Run legacy token generation script (deprecated — use Slack OAuth flow instead)
npm run auth

License

MIT — See LICENSE file for details.

About

Servicio de sincronización de calendarios (Google Calendar + Microsoft Outlook) con integración Slack.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors