Calendar Sync Plugin
A comprehensive example demonstrating calendar synchronization with external systems using the MuluTime Plugin SDK's Action System.
Features
- 🗓️ Bidirectional calendar sync with Google Calendar
- ⚡ Real-time event handling for instant synchronization
- 🔄 Scheduled sync tasks for data consistency
- 🌐 REST API endpoints for external integrations
- 🛡️ Error handling and retry logic
- 📊 Sync statistics and monitoring
Plugin Overview
This plugin demonstrates all four action types in the MuluTime SDK:
- Event Actions - React to booking changes
- Scheduled Actions - Periodic sync operations
- API Actions - External webhook endpoints
- Lifecycle Actions - Setup and cleanup
Complete Implementation
import {
BasePlugin,
PluginManifest,
PluginCategory,
IntegrationType,
PluginPermission,
SystemEventType,
PluginScheduleType,
OnEvent,
Scheduled,
Get,
Post,
OnInstall,
OnUninstall,
SystemEventPayload,
APIRequest,
APIResponse,
PluginContext,
ValidationResult
} from '@mulutime/plugin-sdk';
export class CalendarSyncPlugin extends BasePlugin {
manifest: PluginManifest = {
id: 'com.mulutime.calendar-sync',
name: 'Calendar Sync Plugin',
version: '1.0.0',
description: 'Synchronizes bookings with external calendar systems',
author: {
name: 'MuluTime Team',
email: 'team@mulutime.com'
},
category: PluginCategory.CALENDAR,
type: [IntegrationType.CALENDAR, IntegrationType.AUTOMATION],
permissions: [
PluginPermission.READ_USER_DATA,
PluginPermission.WRITE_USER_DATA,
PluginPermission.EXTERNAL_API_ACCESS,
PluginPermission.SEND_NOTIFICATIONS
],
apiVersion: '1.0.0',
minSystemVersion: '1.0.0',
main: 'index.js'
};
// =================================================================
// LIFECYCLE ACTIONS - Plugin installation and configuration
// =================================================================
@OnInstall()
async handleInstall(context: PluginContext): Promise<void> {
const sdk = this.getSDK();
// Initialize sync database
await sdk.storage.set('sync-mappings', {});
await sdk.storage.set('sync-stats', {
totalSyncs: 0,
lastSyncTime: null,
errors: 0,
success: 0
});
// Validate Google Calendar connection
await this.validateGoogleCalendarConnection();
sdk.logger.info('Calendar sync plugin installed successfully');
}
@OnUninstall()
async handleUninstall(context: PluginContext): Promise<void> {
const sdk = this.getSDK();
// Clean up external calendar events
const mappings = await sdk.storage.get('sync-mappings') || {};
for (const [bookingId, externalId] of Object.entries(mappings)) {
await this.deleteExternalCalendarEvent(externalId as string);
}
// Clear all stored data
const keys = await sdk.storage.list();
for (const key of keys) {
await sdk.storage.delete(key);
}
sdk.logger.info('Calendar sync plugin uninstalled and cleaned up');
}
// =================================================================
// EVENT ACTIONS - Real-time booking synchronization
// =================================================================
@OnEvent(SystemEventType.BOOKING_CREATED, {
priority: 1,
retry: {
attempts: 3,
delay: 1000,
backoff: 'exponential'
}
})
async onBookingCreated(event: SystemEventPayload, context: PluginContext): Promise<void> {
const sdk = this.getSDK();
sdk.logger.info('Processing new booking for calendar sync', {
bookingId: event.data.id,
userId: event.userId
});
try {
// Create external calendar event
const externalEventId = await this.createExternalCalendarEvent({
title: event.data.service.name,
description: event.data.notes || '',
startTime: event.data.startTime,
endTime: event.data.endTime,
attendees: [
{ email: event.data.customer.email, name: event.data.customer.name },
{ email: event.data.provider.email, name: event.data.provider.name }
]
});
// Store sync mapping
const mappings = await sdk.storage.get('sync-mappings') || {};
mappings[event.data.id] = externalEventId;
await sdk.storage.set('sync-mappings', mappings);
// Update stats
await this.updateSyncStats('success');
sdk.logger.info('Booking synced to external calendar', {
bookingId: event.data.id,
externalEventId
});
} catch (error) {
await this.updateSyncStats('error');
sdk.logger.error('Failed to sync booking to calendar', error, {
bookingId: event.data.id
});
throw error; // This will trigger retry logic
}
}
@OnEvent(SystemEventType.BOOKING_UPDATED, {
priority: 1,
timeout: 30000 // 30 second timeout
})
async onBookingUpdated(event: SystemEventPayload, context: PluginContext): Promise<void> {
const sdk = this.getSDK();
const mappings = await sdk.storage.get('sync-mappings') || {};
const externalEventId = mappings[event.data.id];
if (!externalEventId) {
sdk.logger.warn('No external calendar event found for booking', {
bookingId: event.data.id
});
return;
}
try {
// Update external calendar event
await this.updateExternalCalendarEvent(externalEventId, {
title: event.data.service.name,
description: event.data.notes || '',
startTime: event.data.startTime,
endTime: event.data.endTime
});
await this.updateSyncStats('success');
sdk.logger.info('Booking update synced to external calendar', {
bookingId: event.data.id,
externalEventId
});
} catch (error) {
await this.updateSyncStats('error');
sdk.logger.error('Failed to sync booking update to calendar', error);
throw error;
}
}
@OnEvent(SystemEventType.BOOKING_CANCELLED, {
priority: 1
})
async onBookingCancelled(event: SystemEventPayload, context: PluginContext): Promise<void> {
const sdk = this.getSDK();
const mappings = await sdk.storage.get('sync-mappings') || {};
const externalEventId = mappings[event.data.id];
if (!externalEventId) {
return;
}
try {
// Delete external calendar event
await this.deleteExternalCalendarEvent(externalEventId);
// Remove mapping
delete mappings[event.data.id];
await sdk.storage.set('sync-mappings', mappings);
await this.updateSyncStats('success');
sdk.logger.info('Cancelled booking removed from external calendar', {
bookingId: event.data.id,
externalEventId
});
} catch (error) {
await this.updateSyncStats('error');
sdk.logger.error('Failed to remove cancelled booking from calendar', error);
}
}
// =================================================================
// SCHEDULED ACTIONS - Periodic maintenance and sync
// =================================================================
@Scheduled(PluginScheduleType.HOURLY)
async performHourlySync(context: PluginContext): Promise<void> {
const sdk = this.getSDK();
sdk.logger.info('Starting hourly calendar sync');
try {
// Sync external calendar events back to MuluTime
const externalEvents = await this.getExternalCalendarEvents();
for (const externalEvent of externalEvents) {
await this.syncExternalEventToMuluTime(externalEvent);
}
sdk.logger.info(`Hourly sync completed: ${externalEvents.length} events processed`);
} catch (error) {
sdk.logger.error('Hourly sync failed', error);
}
}
@Scheduled(PluginScheduleType.DAILY, { time: '02:00' })
async performDailyMaintenance(context: PluginContext): Promise<void> {
const sdk = this.getSDK();
sdk.logger.info('Starting daily maintenance');
try {
// Clean up orphaned sync mappings
await this.cleanupOrphanedMappings();
// Generate daily sync report
const stats = await sdk.storage.get('sync-stats');
sdk.logger.info('Daily sync statistics', stats);
// Reset daily counters
await sdk.storage.set('sync-stats', {
...stats,
dailySuccess: 0,
dailyErrors: 0,
lastMaintenanceRun: new Date().toISOString()
});
} catch (error) {
sdk.logger.error('Daily maintenance failed', error);
}
}
// =================================================================
// API ACTIONS - External webhook endpoints
// =================================================================
@Get('/sync-status')
async getSyncStatus(request: APIRequest, context: PluginContext): Promise<APIResponse> {
const sdk = this.getSDK();
const stats = await sdk.storage.get('sync-stats') || {};
const mappings = await sdk.storage.get('sync-mappings') || {};
return {
status: 200,
data: {
isConnected: await this.isGoogleCalendarConnected(),
syncStats: stats,
activeMappings: Object.keys(mappings).length,
lastSyncTime: stats.lastSyncTime
}
};
}
@Post('/manual-sync', {
validation: {
body: {
type: 'object',
properties: {
bookingId: { type: 'string' },
force: { type: 'boolean', default: false }
},
required: ['bookingId']
}
},
requiredPermissions: [PluginPermission.WRITE_USER_DATA]
})
async manualSync(request: APIRequest, context: PluginContext): Promise<APIResponse> {
const sdk = this.getSDK();
const { bookingId, force } = request.body;
try {
// Get booking data from MuluTime API
const booking = await sdk.api.get(`/bookings/${bookingId}`);
if (!booking) {
return {
status: 404,
error: 'Booking not found'
};
}
// Check if already synced
const mappings = await sdk.storage.get('sync-mappings') || {};
if (mappings[bookingId] && !force) {
return {
status: 400,
error: 'Booking already synced. Use force=true to re-sync.'
};
}
// Perform sync
const externalEventId = await this.createExternalCalendarEvent({
title: booking.service.name,
description: booking.notes || '',
startTime: booking.startTime,
endTime: booking.endTime,
attendees: [
{ email: booking.customer.email, name: booking.customer.name }
]
});
// Update mapping
mappings[bookingId] = externalEventId;
await sdk.storage.set('sync-mappings', mappings);
await this.updateSyncStats('success');
return {
status: 200,
data: {
bookingId,
externalEventId,
message: 'Booking synced successfully'
}
};
} catch (error) {
await this.updateSyncStats('error');
return {
status: 500,
error: 'Sync failed: ' + error.message
};
}
}
@Post('/webhook/google-calendar')
async handleGoogleCalendarWebhook(request: APIRequest, context: PluginContext): Promise<APIResponse> {
const sdk = this.getSDK();
sdk.logger.info('Received Google Calendar webhook', {
headers: request.headers,
body: request.body
});
try {
// Process Google Calendar webhook
const { eventId, eventType, eventData } = request.body;
switch (eventType) {
case 'event.created':
await this.handleExternalEventCreated(eventData);
break;
case 'event.updated':
await this.handleExternalEventUpdated(eventData);
break;
case 'event.deleted':
await this.handleExternalEventDeleted(eventId);
break;
}
return { status: 200, data: { processed: true } };
} catch (error) {
sdk.logger.error('Failed to process Google Calendar webhook', error);
return { status: 500, error: 'Webhook processing failed' };
}
}
// =================================================================
// HELPER METHODS
// =================================================================
private async validateGoogleCalendarConnection(): Promise<boolean> {
const sdk = this.getSDK();
try {
// Test Google Calendar API connection
const response = await sdk.http.get('https://www.googleapis.com/calendar/v3/calendars/primary', {
headers: {
'Authorization': `Bearer ${sdk.config.googleCalendarApiKey}`
}
});
return response.status === 200;
} catch (error) {
sdk.logger.error('Google Calendar connection validation failed', error);
return false;
}
}
private async createExternalCalendarEvent(eventData: any): Promise<string> {
const sdk = this.getSDK();
const response = await sdk.http.post('https://www.googleapis.com/calendar/v3/calendars/primary/events', {
summary: eventData.title,
description: eventData.description,
start: {
dateTime: eventData.startTime,
timeZone: 'UTC'
},
end: {
dateTime: eventData.endTime,
timeZone: 'UTC'
},
attendees: eventData.attendees
}, {
headers: {
'Authorization': `Bearer ${sdk.config.googleCalendarApiKey}`,
'Content-Type': 'application/json'
}
});
return response.data.id;
}
private async updateExternalCalendarEvent(eventId: string, eventData: any): Promise<void> {
const sdk = this.getSDK();
await sdk.http.put(`https://www.googleapis.com/calendar/v3/calendars/primary/events/${eventId}`, {
summary: eventData.title,
description: eventData.description,
start: {
dateTime: eventData.startTime,
timeZone: 'UTC'
},
end: {
dateTime: eventData.endTime,
timeZone: 'UTC'
}
}, {
headers: {
'Authorization': `Bearer ${sdk.config.googleCalendarApiKey}`,
'Content-Type': 'application/json'
}
});
}
private async deleteExternalCalendarEvent(eventId: string): Promise<void> {
const sdk = this.getSDK();
await sdk.http.delete(`https://www.googleapis.com/calendar/v3/calendars/primary/events/${eventId}`, {
headers: {
'Authorization': `Bearer ${sdk.config.googleCalendarApiKey}`
}
});
}
private async updateSyncStats(type: 'success' | 'error'): Promise<void> {
const sdk = this.getSDK();
const stats = await sdk.storage.get('sync-stats') || {
totalSyncs: 0,
errors: 0,
success: 0
};
stats.totalSyncs++;
stats.lastSyncTime = new Date().toISOString();
if (type === 'success') {
stats.success++;
} else {
stats.errors++;
}
await sdk.storage.set('sync-stats', stats);
}
// Additional helper methods...
private async isGoogleCalendarConnected(): Promise<boolean> {
return await this.validateGoogleCalendarConnection();
}
private async getExternalCalendarEvents(): Promise<any[]> {
// Implementation for fetching external events
return [];
}
private async syncExternalEventToMuluTime(externalEvent: any): Promise<void> {
// Implementation for syncing external events back
}
private async cleanupOrphanedMappings(): Promise<void> {
// Implementation for cleaning up orphaned sync mappings
}
private async handleExternalEventCreated(eventData: any): Promise<void> {
// Implementation for handling external event creation
}
private async handleExternalEventUpdated(eventData: any): Promise<void> {
// Implementation for handling external event updates
}
private async handleExternalEventDeleted(eventId: string): Promise<void> {
// Implementation for handling external event deletion
}
}
// Export the plugin instance
export default new CalendarSyncPlugin();
Configuration
The plugin requires Google Calendar API credentials:
{
"googleCalendarApiKey": "your-google-api-key-here",
"syncDirection": "bidirectional",
"autoSync": true,
"syncInterval": 60
}
Key Features Demonstrated
🎯 Action System Usage
- Event Actions: Real-time booking synchronization
- Scheduled Actions: Hourly sync and daily maintenance
- API Actions: REST endpoints for manual sync and webhooks
- Lifecycle Actions: Installation setup and cleanup
🛡️ Error Handling
- Retry logic with exponential backoff
- Timeout protection for long operations
- Comprehensive error logging
- Graceful degradation
📊 Monitoring
- Sync statistics tracking
- Connection health monitoring
- Performance metrics
- Detailed logging
🔧 External Integration
- Google Calendar API integration
- Webhook handling for bidirectional sync
- Authentication management
- Rate limiting compliance
Installation
- Install the plugin through the MuluTime admin panel
- Configure Google Calendar API credentials
- Set sync preferences (direction, interval, etc.)
- Test the connection using the
/sync-status
endpoint
Usage Examples
Manual Sync via API
curl -X POST "https://your-mulutime-instance.com/plugins/calendar-sync/manual-sync" \
-H "Content-Type: application/json" \
-d '{"bookingId": "booking-123", "force": false}'
Check Sync Status
curl "https://your-mulutime-instance.com/plugins/calendar-sync/sync-status"
This example demonstrates the full power of the MuluTime Action System, showing how to build a production-ready plugin with comprehensive event handling, scheduling, API endpoints, and external integrations.