@soulbatical/tetra-core 0.10.4 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +78 -38
- package/dist/core/createApp.d.ts +1 -1
- package/dist/core/createApp.d.ts.map +1 -1
- package/dist/core/createApp.js +77 -2
- package/dist/core/createApp.js.map +1 -1
- package/dist/core/dualWriteProxy.d.ts +7 -2
- package/dist/core/dualWriteProxy.d.ts.map +1 -1
- package/dist/core/dualWriteProxy.js +16 -5
- package/dist/core/dualWriteProxy.js.map +1 -1
- package/dist/core/routeContext.d.ts +24 -0
- package/dist/core/routeContext.d.ts.map +1 -1
- package/dist/core/routeContext.js +31 -4
- package/dist/core/routeContext.js.map +1 -1
- package/dist/core/systemDb.d.ts +2 -2
- package/dist/core/systemDb.js +2 -2
- package/dist/generators/rls-checker.d.ts +1 -1
- package/dist/generators/rls-checker.js +1 -1
- package/dist/generators/rls-exec-sql.d.ts +1 -1
- package/dist/generators/rls-exec-sql.js +1 -1
- package/dist/generators/rpc/index.d.ts +1 -1
- package/dist/generators/rpc/index.js +1 -1
- package/dist/index.d.ts +3 -31
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -32
- package/dist/index.js.map +1 -1
- package/dist/middleware/securityMiddleware.d.ts +1 -1
- package/dist/middleware/securityMiddleware.d.ts.map +1 -1
- package/dist/middleware/validateBody.d.ts.map +1 -1
- package/dist/middleware/validateBody.js +51 -8
- package/dist/middleware/validateBody.js.map +1 -1
- package/dist/shared/rfc7807ErrorResponse.d.ts +7 -0
- package/dist/shared/rfc7807ErrorResponse.d.ts.map +1 -1
- package/dist/shared/rfc7807ErrorResponse.js +19 -5
- package/dist/shared/rfc7807ErrorResponse.js.map +1 -1
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +16 -1
- package/dist/utils/logger.js.map +1 -1
- package/package.json +33 -77
- package/dist/affiliate.d.ts +0 -11
- package/dist/affiliate.d.ts.map +0 -1
- package/dist/affiliate.js +0 -10
- package/dist/affiliate.js.map +0 -1
- package/dist/billing.d.ts +0 -8
- package/dist/billing.d.ts.map +0 -1
- package/dist/billing.js +0 -7
- package/dist/billing.js.map +0 -1
- package/dist/email.d.ts +0 -9
- package/dist/email.d.ts.map +0 -1
- package/dist/email.js +0 -8
- package/dist/email.js.map +0 -1
- package/dist/generators/rls-exec-sql.sql +0 -57
- package/dist/generators.d.ts +0 -15
- package/dist/generators.d.ts.map +0 -1
- package/dist/generators.js +0 -12
- package/dist/generators.js.map +0 -1
- package/dist/mcp.d.ts +0 -8
- package/dist/mcp.d.ts.map +0 -1
- package/dist/mcp.js +0 -7
- package/dist/mcp.js.map +0 -1
- package/dist/planner.d.ts +0 -8
- package/dist/planner.d.ts.map +0 -1
- package/dist/planner.js +0 -7
- package/dist/planner.js.map +0 -1
- package/dist/shared/affiliate/AffiliateAttributionService.d.ts +0 -47
- package/dist/shared/affiliate/AffiliateAttributionService.d.ts.map +0 -1
- package/dist/shared/affiliate/AffiliateAttributionService.js +0 -308
- package/dist/shared/affiliate/AffiliateAttributionService.js.map +0 -1
- package/dist/shared/affiliate/AffiliateClickService.d.ts +0 -35
- package/dist/shared/affiliate/AffiliateClickService.d.ts.map +0 -1
- package/dist/shared/affiliate/AffiliateClickService.js +0 -87
- package/dist/shared/affiliate/AffiliateClickService.js.map +0 -1
- package/dist/shared/affiliate/affiliateFeatureConfig.d.ts +0 -11
- package/dist/shared/affiliate/affiliateFeatureConfig.d.ts.map +0 -1
- package/dist/shared/affiliate/affiliateFeatureConfig.js +0 -242
- package/dist/shared/affiliate/affiliateFeatureConfig.js.map +0 -1
- package/dist/shared/affiliate/index.d.ts +0 -11
- package/dist/shared/affiliate/index.d.ts.map +0 -1
- package/dist/shared/affiliate/index.js +0 -13
- package/dist/shared/affiliate/index.js.map +0 -1
- package/dist/shared/affiliate/routes.d.ts +0 -87
- package/dist/shared/affiliate/routes.d.ts.map +0 -1
- package/dist/shared/affiliate/routes.js +0 -404
- package/dist/shared/affiliate/routes.js.map +0 -1
- package/dist/shared/affiliate/types.d.ts +0 -170
- package/dist/shared/affiliate/types.d.ts.map +0 -1
- package/dist/shared/affiliate/types.js +0 -11
- package/dist/shared/affiliate/types.js.map +0 -1
- package/dist/shared/billing/BillingService.d.ts +0 -56
- package/dist/shared/billing/BillingService.d.ts.map +0 -1
- package/dist/shared/billing/BillingService.js +0 -588
- package/dist/shared/billing/BillingService.js.map +0 -1
- package/dist/shared/billing/SeatBillingService.d.ts +0 -106
- package/dist/shared/billing/SeatBillingService.d.ts.map +0 -1
- package/dist/shared/billing/SeatBillingService.js +0 -292
- package/dist/shared/billing/SeatBillingService.js.map +0 -1
- package/dist/shared/billing/index.d.ts +0 -30
- package/dist/shared/billing/index.d.ts.map +0 -1
- package/dist/shared/billing/index.js +0 -27
- package/dist/shared/billing/index.js.map +0 -1
- package/dist/shared/billing/routes.d.ts +0 -45
- package/dist/shared/billing/routes.d.ts.map +0 -1
- package/dist/shared/billing/routes.js +0 -184
- package/dist/shared/billing/routes.js.map +0 -1
- package/dist/shared/billing/seat-pricing.d.ts +0 -53
- package/dist/shared/billing/seat-pricing.d.ts.map +0 -1
- package/dist/shared/billing/seat-pricing.js +0 -81
- package/dist/shared/billing/seat-pricing.js.map +0 -1
- package/dist/shared/billing/types.d.ts +0 -109
- package/dist/shared/billing/types.d.ts.map +0 -1
- package/dist/shared/billing/types.js +0 -8
- package/dist/shared/billing/types.js.map +0 -1
- package/dist/shared/email/EmailService.d.ts +0 -64
- package/dist/shared/email/EmailService.d.ts.map +0 -1
- package/dist/shared/email/EmailService.js +0 -300
- package/dist/shared/email/EmailService.js.map +0 -1
- package/dist/shared/email/adminRoutes.d.ts +0 -30
- package/dist/shared/email/adminRoutes.d.ts.map +0 -1
- package/dist/shared/email/adminRoutes.js +0 -227
- package/dist/shared/email/adminRoutes.js.map +0 -1
- package/dist/shared/email/gmail.d.ts +0 -208
- package/dist/shared/email/gmail.d.ts.map +0 -1
- package/dist/shared/email/gmail.js +0 -626
- package/dist/shared/email/gmail.js.map +0 -1
- package/dist/shared/email/index.d.ts +0 -15
- package/dist/shared/email/index.d.ts.map +0 -1
- package/dist/shared/email/index.js +0 -18
- package/dist/shared/email/index.js.map +0 -1
- package/dist/shared/email/mailgun.d.ts +0 -18
- package/dist/shared/email/mailgun.d.ts.map +0 -1
- package/dist/shared/email/mailgun.js +0 -76
- package/dist/shared/email/mailgun.js.map +0 -1
- package/dist/shared/email/sanitize.d.ts +0 -25
- package/dist/shared/email/sanitize.d.ts.map +0 -1
- package/dist/shared/email/sanitize.js +0 -39
- package/dist/shared/email/sanitize.js.map +0 -1
- package/dist/shared/email/smtp.d.ts +0 -20
- package/dist/shared/email/smtp.d.ts.map +0 -1
- package/dist/shared/email/smtp.js +0 -53
- package/dist/shared/email/smtp.js.map +0 -1
- package/dist/shared/email/types.d.ts +0 -113
- package/dist/shared/email/types.d.ts.map +0 -1
- package/dist/shared/email/types.js +0 -7
- package/dist/shared/email/types.js.map +0 -1
- package/dist/shared/email/webhookRoutes.d.ts +0 -29
- package/dist/shared/email/webhookRoutes.d.ts.map +0 -1
- package/dist/shared/email/webhookRoutes.js +0 -125
- package/dist/shared/email/webhookRoutes.js.map +0 -1
- package/dist/shared/mcp/index.d.ts +0 -51
- package/dist/shared/mcp/index.d.ts.map +0 -1
- package/dist/shared/mcp/index.js +0 -51
- package/dist/shared/mcp/index.js.map +0 -1
- package/dist/shared/mcp/mcp-auth-routes.d.ts +0 -26
- package/dist/shared/mcp/mcp-auth-routes.d.ts.map +0 -1
- package/dist/shared/mcp/mcp-auth-routes.js +0 -141
- package/dist/shared/mcp/mcp-auth-routes.js.map +0 -1
- package/dist/shared/mcp/mcp-db.d.ts +0 -99
- package/dist/shared/mcp/mcp-db.d.ts.map +0 -1
- package/dist/shared/mcp/mcp-db.js +0 -106
- package/dist/shared/mcp/mcp-db.js.map +0 -1
- package/dist/shared/mcp/mcp-routes.d.ts +0 -29
- package/dist/shared/mcp/mcp-routes.d.ts.map +0 -1
- package/dist/shared/mcp/mcp-routes.js +0 -171
- package/dist/shared/mcp/mcp-routes.js.map +0 -1
- package/dist/shared/mcp/mcp-tokens-routes.d.ts +0 -35
- package/dist/shared/mcp/mcp-tokens-routes.d.ts.map +0 -1
- package/dist/shared/mcp/mcp-tokens-routes.js +0 -94
- package/dist/shared/mcp/mcp-tokens-routes.js.map +0 -1
- package/dist/shared/mcp/mcp-usage-routes.d.ts +0 -17
- package/dist/shared/mcp/mcp-usage-routes.d.ts.map +0 -1
- package/dist/shared/mcp/mcp-usage-routes.js +0 -81
- package/dist/shared/mcp/mcp-usage-routes.js.map +0 -1
- package/dist/shared/mcp/tenant-context.d.ts +0 -59
- package/dist/shared/mcp/tenant-context.d.ts.map +0 -1
- package/dist/shared/mcp/tenant-context.js +0 -136
- package/dist/shared/mcp/tenant-context.js.map +0 -1
- package/dist/shared/mcp/types.d.ts +0 -74
- package/dist/shared/mcp/types.d.ts.map +0 -1
- package/dist/shared/mcp/types.js +0 -7
- package/dist/shared/mcp/types.js.map +0 -1
- package/dist/shared/planner/GoogleCalendarService.d.ts +0 -137
- package/dist/shared/planner/GoogleCalendarService.d.ts.map +0 -1
- package/dist/shared/planner/GoogleCalendarService.js +0 -525
- package/dist/shared/planner/GoogleCalendarService.js.map +0 -1
- package/dist/shared/planner/PlannerService.d.ts +0 -264
- package/dist/shared/planner/PlannerService.d.ts.map +0 -1
- package/dist/shared/planner/PlannerService.js +0 -1393
- package/dist/shared/planner/PlannerService.js.map +0 -1
- package/dist/shared/planner/index.d.ts +0 -37
- package/dist/shared/planner/index.d.ts.map +0 -1
- package/dist/shared/planner/index.js +0 -35
- package/dist/shared/planner/index.js.map +0 -1
- package/dist/shared/planner/intervals.d.ts +0 -60
- package/dist/shared/planner/intervals.d.ts.map +0 -1
- package/dist/shared/planner/intervals.js +0 -141
- package/dist/shared/planner/intervals.js.map +0 -1
- package/dist/shared/planner/routes.d.ts +0 -69
- package/dist/shared/planner/routes.d.ts.map +0 -1
- package/dist/shared/planner/routes.js +0 -770
- package/dist/shared/planner/routes.js.map +0 -1
- package/dist/shared/planner/types.d.ts +0 -328
- package/dist/shared/planner/types.d.ts.map +0 -1
- package/dist/shared/planner/types.js +0 -9
- package/dist/shared/planner/types.js.map +0 -1
- package/dist/shared/storage/ImageProcessingService.d.ts +0 -32
- package/dist/shared/storage/ImageProcessingService.d.ts.map +0 -1
- package/dist/shared/storage/ImageProcessingService.js +0 -127
- package/dist/shared/storage/ImageProcessingService.js.map +0 -1
- package/dist/shared/storage/StorageProxyService.d.ts +0 -47
- package/dist/shared/storage/StorageProxyService.d.ts.map +0 -1
- package/dist/shared/storage/StorageProxyService.js +0 -196
- package/dist/shared/storage/StorageProxyService.js.map +0 -1
- package/dist/shared/storage/StorageUploadService.d.ts +0 -126
- package/dist/shared/storage/StorageUploadService.d.ts.map +0 -1
- package/dist/shared/storage/StorageUploadService.js +0 -206
- package/dist/shared/storage/StorageUploadService.js.map +0 -1
- package/dist/shared/storage/creative-urls.d.ts +0 -14
- package/dist/shared/storage/creative-urls.d.ts.map +0 -1
- package/dist/shared/storage/creative-urls.js +0 -30
- package/dist/shared/storage/creative-urls.js.map +0 -1
- package/dist/shared/storage/index.d.ts +0 -28
- package/dist/shared/storage/index.d.ts.map +0 -1
- package/dist/shared/storage/index.js +0 -27
- package/dist/shared/storage/index.js.map +0 -1
- package/dist/shared/storage/routes.d.ts +0 -42
- package/dist/shared/storage/routes.d.ts.map +0 -1
- package/dist/shared/storage/routes.js +0 -160
- package/dist/shared/storage/routes.js.map +0 -1
- package/dist/shared/storage/types.d.ts +0 -53
- package/dist/shared/storage/types.d.ts.map +0 -1
- package/dist/shared/storage/types.js +0 -2
- package/dist/shared/storage/types.js.map +0 -1
- package/dist/shared/telegram/index.d.ts +0 -4
- package/dist/shared/telegram/index.d.ts.map +0 -1
- package/dist/shared/telegram/index.js +0 -3
- package/dist/shared/telegram/index.js.map +0 -1
- package/dist/shared/telegram/routes.d.ts +0 -43
- package/dist/shared/telegram/routes.d.ts.map +0 -1
- package/dist/shared/telegram/routes.js +0 -868
- package/dist/shared/telegram/routes.js.map +0 -1
- package/dist/shared/telegram/types.d.ts +0 -168
- package/dist/shared/telegram/types.d.ts.map +0 -1
- package/dist/shared/telegram/types.js +0 -7
- package/dist/shared/telegram/types.js.map +0 -1
- package/dist/shared/telegram/utils.d.ts +0 -44
- package/dist/shared/telegram/utils.d.ts.map +0 -1
- package/dist/shared/telegram/utils.js +0 -121
- package/dist/shared/telegram/utils.js.map +0 -1
- package/dist/storage.d.ts +0 -9
- package/dist/storage.d.ts.map +0 -1
- package/dist/storage.js +0 -8
- package/dist/storage.js.map +0 -1
- package/dist/telemetry.d.ts +0 -9
- package/dist/telemetry.d.ts.map +0 -1
- package/dist/telemetry.js +0 -8
- package/dist/telemetry.js.map +0 -1
- package/scripts/postinstall.js +0 -79
- package/src/shared/affiliate/migrations/001_create_affiliates.sql +0 -49
- package/src/shared/affiliate/migrations/002_create_affiliate_commissions.sql +0 -31
- package/src/shared/affiliate/migrations/003_create_affiliate_clicks.sql +0 -26
- package/src/shared/affiliate/migrations/004_create_affiliate_payments.sql +0 -34
- package/src/shared/affiliate/migrations/005_create_affiliate_tier_history.sql +0 -19
- package/src/shared/affiliate/migrations/006_create_affiliate_rpc_functions.sql +0 -209
- package/src/shared/affiliate/migrations/007_create_affiliate_rls_policies.sql +0 -123
- package/src/shared/billing/migrations/00000000000001_billing.sql +0 -114
- package/src/shared/email/migrations/000_create_email_logs.sql +0 -27
- package/src/shared/email/migrations/001_create_email_templates.sql +0 -27
- package/src/shared/email/migrations/002_add_rls_baseline_policies.sql +0 -37
- package/src/shared/email/migrations/003_create_gmail_accounts.sql +0 -82
- package/src/shared/email/migrations/004_add_email_logs_tracking_columns.sql +0 -15
- package/src/shared/mcp/migrations/001_mcp_api_tokens.sql +0 -21
- package/src/shared/mcp/migrations/002_mcp_audit_log.sql +0 -16
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* GoogleCalendarService — Google Calendar OAuth + event sync
|
|
3
|
-
*
|
|
4
|
-
* Handles:
|
|
5
|
-
* - OAuth2 consent URL generation
|
|
6
|
-
* - Token exchange and storage
|
|
7
|
-
* - Token auto-refresh
|
|
8
|
-
* - Calendar event CRUD (create, update, delete)
|
|
9
|
-
* - Calendar listing and selection
|
|
10
|
-
* - Connection status check
|
|
11
|
-
*
|
|
12
|
-
* Uses lazy import for googleapis to avoid requiring it in projects that don't use calendar sync.
|
|
13
|
-
*
|
|
14
|
-
* Usage:
|
|
15
|
-
* ```typescript
|
|
16
|
-
* import { GoogleCalendarService } from '@soulbatical/tetra-core';
|
|
17
|
-
*
|
|
18
|
-
* const calService = new GoogleCalendarService({
|
|
19
|
-
* google: { clientId, clientSecret, redirectUri },
|
|
20
|
-
* });
|
|
21
|
-
* const url = calService.getAuthUrl(userId);
|
|
22
|
-
* ```
|
|
23
|
-
*/
|
|
24
|
-
import type { PlannerCalendarConfig } from './types.js';
|
|
25
|
-
interface AppointmentEventData {
|
|
26
|
-
title: string;
|
|
27
|
-
description?: string;
|
|
28
|
-
startTime: string;
|
|
29
|
-
endTime: string;
|
|
30
|
-
location?: string;
|
|
31
|
-
type?: string;
|
|
32
|
-
}
|
|
33
|
-
export declare class GoogleCalendarService {
|
|
34
|
-
private config;
|
|
35
|
-
private googleModule;
|
|
36
|
-
constructor(config: PlannerCalendarConfig);
|
|
37
|
-
private getGoogle;
|
|
38
|
-
private getSystemDB;
|
|
39
|
-
private get scopes();
|
|
40
|
-
private get timezone();
|
|
41
|
-
private createOAuth2Client;
|
|
42
|
-
/**
|
|
43
|
-
* Get an authenticated OAuth2 client for a user.
|
|
44
|
-
* Auto-refreshes expired tokens and persists new ones.
|
|
45
|
-
*/
|
|
46
|
-
private getAuthenticatedClient;
|
|
47
|
-
/**
|
|
48
|
-
* Get the user's selected calendar ID (legacy single-select).
|
|
49
|
-
*/
|
|
50
|
-
private getCalendarId;
|
|
51
|
-
/**
|
|
52
|
-
* Get the user's selected calendar IDs (multi-select).
|
|
53
|
-
* Falls back to the single calendar_id if selected_calendar_ids is not set.
|
|
54
|
-
*/
|
|
55
|
-
getSelectedCalendarIds(userId: string): Promise<string[]>;
|
|
56
|
-
/**
|
|
57
|
-
* Generate Google OAuth2 consent URL.
|
|
58
|
-
*/
|
|
59
|
-
getAuthUrl(userId: string, state?: string): Promise<string>;
|
|
60
|
-
/**
|
|
61
|
-
* Exchange authorization code for tokens and store them.
|
|
62
|
-
*/
|
|
63
|
-
handleCallback(code: string, userId: string, organizationId: string): Promise<void>;
|
|
64
|
-
/**
|
|
65
|
-
* Create a Google Calendar event. Returns event ID or null.
|
|
66
|
-
*/
|
|
67
|
-
createEvent(userId: string, data: AppointmentEventData): Promise<string | null>;
|
|
68
|
-
/**
|
|
69
|
-
* Update a Google Calendar event.
|
|
70
|
-
*/
|
|
71
|
-
updateEvent(userId: string, eventId: string, data: AppointmentEventData): Promise<void>;
|
|
72
|
-
/**
|
|
73
|
-
* Delete a Google Calendar event.
|
|
74
|
-
*/
|
|
75
|
-
deleteEvent(userId: string, eventId: string): Promise<void>;
|
|
76
|
-
/**
|
|
77
|
-
* List all calendars the user has write access to.
|
|
78
|
-
*/
|
|
79
|
-
listCalendars(userId: string): Promise<Array<{
|
|
80
|
-
id: string;
|
|
81
|
-
name: string;
|
|
82
|
-
isPrimary: boolean;
|
|
83
|
-
color: string;
|
|
84
|
-
isSelected: boolean;
|
|
85
|
-
}> | null>;
|
|
86
|
-
/**
|
|
87
|
-
* Set which Google Calendar to sync to.
|
|
88
|
-
*/
|
|
89
|
-
selectCalendar(userId: string, calendarId: string): Promise<boolean>;
|
|
90
|
-
/**
|
|
91
|
-
* Set multiple calendars for conflict checking (multi-select).
|
|
92
|
-
* Also updates the primary calendar_id to the first selected.
|
|
93
|
-
*/
|
|
94
|
-
selectCalendars(userId: string, calendarIds: string[]): Promise<boolean>;
|
|
95
|
-
/**
|
|
96
|
-
* Check if a user has an active Google Calendar connection.
|
|
97
|
-
*/
|
|
98
|
-
isConnected(userId: string): Promise<boolean>;
|
|
99
|
-
/**
|
|
100
|
-
* Disconnect Google Calendar for a user.
|
|
101
|
-
*/
|
|
102
|
-
disconnect(userId: string): Promise<void>;
|
|
103
|
-
/**
|
|
104
|
-
* Fetch events from ALL selected Google Calendars for a date range.
|
|
105
|
-
* Returns busy time blocks merged across all selected calendars.
|
|
106
|
-
*
|
|
107
|
-
* This replaces the legacy single-calendar getBusySlots behaviour:
|
|
108
|
-
* if the user has multiple calendars selected via selected_calendar_ids,
|
|
109
|
-
* busy events from all of them are returned.
|
|
110
|
-
*/
|
|
111
|
-
getBusySlots(userId: string, dateStart: string, dateEnd: string): Promise<Array<{
|
|
112
|
-
start_time: string;
|
|
113
|
-
end_time: string;
|
|
114
|
-
summary?: string;
|
|
115
|
-
calendarId?: string;
|
|
116
|
-
calendarName?: string;
|
|
117
|
-
}>>;
|
|
118
|
-
/**
|
|
119
|
-
* Register a Google Calendar push notification channel for real-time event updates.
|
|
120
|
-
* Returns the channel info that should be stored for later teardown.
|
|
121
|
-
* Requires a publicly accessible HTTPS webhook URL.
|
|
122
|
-
*
|
|
123
|
-
* Note: Google Calendar push channels expire after 1 hour to 30 days depending on type.
|
|
124
|
-
* Caller is responsible for periodic renewal.
|
|
125
|
-
*/
|
|
126
|
-
watchCalendar(userId: string, calendarId: string, webhookUrl: string, channelId: string, expirationMs?: number): Promise<{
|
|
127
|
-
channelId: string;
|
|
128
|
-
resourceId: string;
|
|
129
|
-
expiration: string;
|
|
130
|
-
} | null>;
|
|
131
|
-
/**
|
|
132
|
-
* Stop a previously registered push notification channel.
|
|
133
|
-
*/
|
|
134
|
-
stopWatch(userId: string, channelId: string, resourceId: string): Promise<boolean>;
|
|
135
|
-
}
|
|
136
|
-
export {};
|
|
137
|
-
//# sourceMappingURL=GoogleCalendarService.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"GoogleCalendarService.d.ts","sourceRoot":"","sources":["../../../src/shared/planner/GoogleCalendarService.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,525 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* GoogleCalendarService — Google Calendar OAuth + event sync
|
|
3
|
-
*
|
|
4
|
-
* Handles:
|
|
5
|
-
* - OAuth2 consent URL generation
|
|
6
|
-
* - Token exchange and storage
|
|
7
|
-
* - Token auto-refresh
|
|
8
|
-
* - Calendar event CRUD (create, update, delete)
|
|
9
|
-
* - Calendar listing and selection
|
|
10
|
-
* - Connection status check
|
|
11
|
-
*
|
|
12
|
-
* Uses lazy import for googleapis to avoid requiring it in projects that don't use calendar sync.
|
|
13
|
-
*
|
|
14
|
-
* Usage:
|
|
15
|
-
* ```typescript
|
|
16
|
-
* import { GoogleCalendarService } from '@soulbatical/tetra-core';
|
|
17
|
-
*
|
|
18
|
-
* const calService = new GoogleCalendarService({
|
|
19
|
-
* google: { clientId, clientSecret, redirectUri },
|
|
20
|
-
* });
|
|
21
|
-
* const url = calService.getAuthUrl(userId);
|
|
22
|
-
* ```
|
|
23
|
-
*/
|
|
24
|
-
import { createLogger } from '../../utils/logger.js';
|
|
25
|
-
import { systemDB as defaultSystemDB } from '../../core/systemDb.js';
|
|
26
|
-
const logger = createLogger('planner:google-calendar');
|
|
27
|
-
const DEFAULT_SCOPES = [
|
|
28
|
-
'https://www.googleapis.com/auth/calendar.events',
|
|
29
|
-
'https://www.googleapis.com/auth/calendar.readonly',
|
|
30
|
-
];
|
|
31
|
-
export class GoogleCalendarService {
|
|
32
|
-
config;
|
|
33
|
-
googleModule = null;
|
|
34
|
-
constructor(config) {
|
|
35
|
-
this.config = config;
|
|
36
|
-
}
|
|
37
|
-
// ─── Lazy googleapis import ───────────────────────────────
|
|
38
|
-
async getGoogle() {
|
|
39
|
-
if (!this.googleModule) {
|
|
40
|
-
try {
|
|
41
|
-
// Dynamic import to avoid requiring googleapis as a hard dependency.
|
|
42
|
-
// Projects that use Google Calendar must install googleapis themselves.
|
|
43
|
-
const moduleName = 'googleapis';
|
|
44
|
-
this.googleModule = await (Function('moduleName', 'return import(moduleName)')(moduleName));
|
|
45
|
-
}
|
|
46
|
-
catch {
|
|
47
|
-
throw new Error('googleapis package is required for Google Calendar integration. Install it: npm install googleapis');
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
return this.googleModule.google;
|
|
51
|
-
}
|
|
52
|
-
async getSystemDB() {
|
|
53
|
-
if (this.config.getSystemDB) {
|
|
54
|
-
return this.config.getSystemDB();
|
|
55
|
-
}
|
|
56
|
-
return defaultSystemDB('planner:calendar');
|
|
57
|
-
}
|
|
58
|
-
get scopes() {
|
|
59
|
-
return this.config.scopes || DEFAULT_SCOPES;
|
|
60
|
-
}
|
|
61
|
-
get timezone() {
|
|
62
|
-
return this.config.timezone || 'Europe/Amsterdam';
|
|
63
|
-
}
|
|
64
|
-
async createOAuth2Client() {
|
|
65
|
-
const google = await this.getGoogle();
|
|
66
|
-
const { clientId, clientSecret, redirectUri } = this.config.google;
|
|
67
|
-
return new google.auth.OAuth2(clientId, clientSecret, redirectUri);
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Get an authenticated OAuth2 client for a user.
|
|
71
|
-
* Auto-refreshes expired tokens and persists new ones.
|
|
72
|
-
*/
|
|
73
|
-
async getAuthenticatedClient(userId) {
|
|
74
|
-
const google = await this.getGoogle();
|
|
75
|
-
const db = await this.getSystemDB();
|
|
76
|
-
const { clientId, clientSecret, redirectUri } = this.config.google;
|
|
77
|
-
const { data: tokenRow, error } = await db
|
|
78
|
-
.from('planner_calendar_tokens')
|
|
79
|
-
.select('*')
|
|
80
|
-
.eq('user_id', userId)
|
|
81
|
-
.eq('is_active', true)
|
|
82
|
-
.single();
|
|
83
|
-
if (error || !tokenRow) {
|
|
84
|
-
logger.debug({ userId }, 'No calendar tokens found');
|
|
85
|
-
return null;
|
|
86
|
-
}
|
|
87
|
-
const oauth2Client = new google.auth.OAuth2(clientId, clientSecret, redirectUri);
|
|
88
|
-
oauth2Client.setCredentials({
|
|
89
|
-
access_token: tokenRow.access_token,
|
|
90
|
-
refresh_token: tokenRow.refresh_token,
|
|
91
|
-
expiry_date: new Date(tokenRow.token_expiry).getTime(),
|
|
92
|
-
});
|
|
93
|
-
// Auto-refresh if expired or expiring within 5 minutes
|
|
94
|
-
const now = Date.now();
|
|
95
|
-
const expiry = new Date(tokenRow.token_expiry).getTime();
|
|
96
|
-
if (now >= expiry - 5 * 60 * 1000) {
|
|
97
|
-
try {
|
|
98
|
-
const { credentials } = await oauth2Client.refreshAccessToken();
|
|
99
|
-
oauth2Client.setCredentials(credentials);
|
|
100
|
-
await db
|
|
101
|
-
.from('planner_calendar_tokens')
|
|
102
|
-
.update({
|
|
103
|
-
access_token: credentials.access_token,
|
|
104
|
-
refresh_token: credentials.refresh_token || tokenRow.refresh_token,
|
|
105
|
-
token_expiry: credentials.expiry_date
|
|
106
|
-
? new Date(credentials.expiry_date).toISOString()
|
|
107
|
-
: tokenRow.token_expiry,
|
|
108
|
-
updated_at: new Date().toISOString(),
|
|
109
|
-
})
|
|
110
|
-
.eq('user_id', userId);
|
|
111
|
-
logger.debug({ userId }, 'Calendar token refreshed');
|
|
112
|
-
}
|
|
113
|
-
catch (refreshError) {
|
|
114
|
-
logger.error({ error: refreshError, userId }, 'Failed to refresh calendar token');
|
|
115
|
-
await db
|
|
116
|
-
.from('planner_calendar_tokens')
|
|
117
|
-
.update({ is_active: false, updated_at: new Date().toISOString() })
|
|
118
|
-
.eq('user_id', userId);
|
|
119
|
-
return null;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
return oauth2Client;
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* Get the user's selected calendar ID (legacy single-select).
|
|
126
|
-
*/
|
|
127
|
-
async getCalendarId(userId) {
|
|
128
|
-
const db = await this.getSystemDB();
|
|
129
|
-
const { data } = await db
|
|
130
|
-
.from('planner_calendar_tokens')
|
|
131
|
-
.select('calendar_id')
|
|
132
|
-
.eq('user_id', userId)
|
|
133
|
-
.single();
|
|
134
|
-
return data?.calendar_id || 'primary';
|
|
135
|
-
}
|
|
136
|
-
/**
|
|
137
|
-
* Get the user's selected calendar IDs (multi-select).
|
|
138
|
-
* Falls back to the single calendar_id if selected_calendar_ids is not set.
|
|
139
|
-
*/
|
|
140
|
-
async getSelectedCalendarIds(userId) {
|
|
141
|
-
const db = await this.getSystemDB();
|
|
142
|
-
const { data } = await db
|
|
143
|
-
.from('planner_calendar_tokens')
|
|
144
|
-
.select('calendar_id, selected_calendar_ids')
|
|
145
|
-
.eq('user_id', userId)
|
|
146
|
-
.single();
|
|
147
|
-
if (data?.selected_calendar_ids && Array.isArray(data.selected_calendar_ids) && data.selected_calendar_ids.length > 0) {
|
|
148
|
-
return data.selected_calendar_ids;
|
|
149
|
-
}
|
|
150
|
-
return [data?.calendar_id || 'primary'];
|
|
151
|
-
}
|
|
152
|
-
// ─── Public API ───────────────────────────────────────────
|
|
153
|
-
/**
|
|
154
|
-
* Generate Google OAuth2 consent URL.
|
|
155
|
-
*/
|
|
156
|
-
async getAuthUrl(userId, state) {
|
|
157
|
-
const oauth2Client = await this.createOAuth2Client();
|
|
158
|
-
const statePayload = JSON.stringify({ userId, custom: state });
|
|
159
|
-
return oauth2Client.generateAuthUrl({
|
|
160
|
-
access_type: 'offline',
|
|
161
|
-
prompt: 'consent',
|
|
162
|
-
scope: this.scopes,
|
|
163
|
-
state: Buffer.from(statePayload).toString('base64'),
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
/**
|
|
167
|
-
* Exchange authorization code for tokens and store them.
|
|
168
|
-
*/
|
|
169
|
-
async handleCallback(code, userId, organizationId) {
|
|
170
|
-
const oauth2Client = await this.createOAuth2Client();
|
|
171
|
-
const { tokens } = await oauth2Client.getToken(code);
|
|
172
|
-
if (!tokens.access_token || !tokens.refresh_token) {
|
|
173
|
-
throw new Error('Failed to obtain tokens from Google');
|
|
174
|
-
}
|
|
175
|
-
const db = await this.getSystemDB();
|
|
176
|
-
const { error } = await db
|
|
177
|
-
.from('planner_calendar_tokens')
|
|
178
|
-
.upsert({
|
|
179
|
-
user_id: userId,
|
|
180
|
-
organization_id: organizationId,
|
|
181
|
-
access_token: tokens.access_token,
|
|
182
|
-
refresh_token: tokens.refresh_token,
|
|
183
|
-
token_expiry: tokens.expiry_date
|
|
184
|
-
? new Date(tokens.expiry_date).toISOString()
|
|
185
|
-
: new Date(Date.now() + 3600 * 1000).toISOString(),
|
|
186
|
-
is_active: true,
|
|
187
|
-
updated_at: new Date().toISOString(),
|
|
188
|
-
}, { onConflict: 'user_id' });
|
|
189
|
-
if (error) {
|
|
190
|
-
logger.error({ error, userId }, 'Failed to store calendar tokens');
|
|
191
|
-
throw new Error('Failed to store calendar tokens');
|
|
192
|
-
}
|
|
193
|
-
logger.info({ userId }, 'Google Calendar connected');
|
|
194
|
-
}
|
|
195
|
-
/**
|
|
196
|
-
* Create a Google Calendar event. Returns event ID or null.
|
|
197
|
-
*/
|
|
198
|
-
async createEvent(userId, data) {
|
|
199
|
-
try {
|
|
200
|
-
const oauth2Client = await this.getAuthenticatedClient(userId);
|
|
201
|
-
if (!oauth2Client)
|
|
202
|
-
return null;
|
|
203
|
-
const google = await this.getGoogle();
|
|
204
|
-
const calendar = google.calendar({ version: 'v3', auth: oauth2Client });
|
|
205
|
-
const calendarId = await this.getCalendarId(userId);
|
|
206
|
-
const response = await calendar.events.insert({
|
|
207
|
-
calendarId,
|
|
208
|
-
requestBody: {
|
|
209
|
-
summary: data.title,
|
|
210
|
-
description: data.description || undefined,
|
|
211
|
-
location: data.location || undefined,
|
|
212
|
-
start: { dateTime: data.startTime, timeZone: this.timezone },
|
|
213
|
-
end: { dateTime: data.endTime, timeZone: this.timezone },
|
|
214
|
-
},
|
|
215
|
-
});
|
|
216
|
-
logger.info({ userId, eventId: response.data.id }, 'Calendar event created');
|
|
217
|
-
return response.data.id || null;
|
|
218
|
-
}
|
|
219
|
-
catch (error) {
|
|
220
|
-
logger.error({ error, userId }, 'Failed to create calendar event');
|
|
221
|
-
return null;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
/**
|
|
225
|
-
* Update a Google Calendar event.
|
|
226
|
-
*/
|
|
227
|
-
async updateEvent(userId, eventId, data) {
|
|
228
|
-
try {
|
|
229
|
-
const oauth2Client = await this.getAuthenticatedClient(userId);
|
|
230
|
-
if (!oauth2Client)
|
|
231
|
-
return;
|
|
232
|
-
const google = await this.getGoogle();
|
|
233
|
-
const calendar = google.calendar({ version: 'v3', auth: oauth2Client });
|
|
234
|
-
const calendarId = await this.getCalendarId(userId);
|
|
235
|
-
await calendar.events.update({
|
|
236
|
-
calendarId,
|
|
237
|
-
eventId,
|
|
238
|
-
requestBody: {
|
|
239
|
-
summary: data.title,
|
|
240
|
-
description: data.description || undefined,
|
|
241
|
-
location: data.location || undefined,
|
|
242
|
-
start: { dateTime: data.startTime, timeZone: this.timezone },
|
|
243
|
-
end: { dateTime: data.endTime, timeZone: this.timezone },
|
|
244
|
-
},
|
|
245
|
-
});
|
|
246
|
-
logger.info({ userId, eventId }, 'Calendar event updated');
|
|
247
|
-
}
|
|
248
|
-
catch (error) {
|
|
249
|
-
logger.error({ error, userId, eventId }, 'Failed to update calendar event');
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
/**
|
|
253
|
-
* Delete a Google Calendar event.
|
|
254
|
-
*/
|
|
255
|
-
async deleteEvent(userId, eventId) {
|
|
256
|
-
try {
|
|
257
|
-
const oauth2Client = await this.getAuthenticatedClient(userId);
|
|
258
|
-
if (!oauth2Client)
|
|
259
|
-
return;
|
|
260
|
-
const google = await this.getGoogle();
|
|
261
|
-
const calendar = google.calendar({ version: 'v3', auth: oauth2Client });
|
|
262
|
-
const calendarId = await this.getCalendarId(userId);
|
|
263
|
-
await calendar.events.delete({ calendarId, eventId });
|
|
264
|
-
logger.info({ userId, eventId }, 'Calendar event deleted');
|
|
265
|
-
}
|
|
266
|
-
catch (error) {
|
|
267
|
-
logger.error({ error, userId, eventId }, 'Failed to delete calendar event');
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
/**
|
|
271
|
-
* List all calendars the user has write access to.
|
|
272
|
-
*/
|
|
273
|
-
async listCalendars(userId) {
|
|
274
|
-
try {
|
|
275
|
-
const oauth2Client = await this.getAuthenticatedClient(userId);
|
|
276
|
-
if (!oauth2Client)
|
|
277
|
-
return null;
|
|
278
|
-
const google = await this.getGoogle();
|
|
279
|
-
const calendar = google.calendar({ version: 'v3', auth: oauth2Client });
|
|
280
|
-
const response = await calendar.calendarList.list({ minAccessRole: 'writer' });
|
|
281
|
-
const items = response.data.items || [];
|
|
282
|
-
const selectedIds = await this.getSelectedCalendarIds(userId);
|
|
283
|
-
return items.map((cal) => {
|
|
284
|
-
const calId = cal.id || 'primary';
|
|
285
|
-
return {
|
|
286
|
-
id: calId,
|
|
287
|
-
name: cal.summary || 'Unnamed',
|
|
288
|
-
isPrimary: cal.primary === true,
|
|
289
|
-
color: cal.backgroundColor || '#4285f4',
|
|
290
|
-
isSelected: selectedIds.includes(calId) || (cal.primary && selectedIds.includes('primary')),
|
|
291
|
-
};
|
|
292
|
-
});
|
|
293
|
-
}
|
|
294
|
-
catch (error) {
|
|
295
|
-
logger.error({ error, userId }, 'Failed to list calendars');
|
|
296
|
-
return null;
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
/**
|
|
300
|
-
* Set which Google Calendar to sync to.
|
|
301
|
-
*/
|
|
302
|
-
async selectCalendar(userId, calendarId) {
|
|
303
|
-
try {
|
|
304
|
-
const db = await this.getSystemDB();
|
|
305
|
-
const { error } = await db
|
|
306
|
-
.from('planner_calendar_tokens')
|
|
307
|
-
.update({ calendar_id: calendarId, updated_at: new Date().toISOString() })
|
|
308
|
-
.eq('user_id', userId)
|
|
309
|
-
.eq('is_active', true);
|
|
310
|
-
if (error) {
|
|
311
|
-
logger.error({ error, userId, calendarId }, 'Failed to select calendar');
|
|
312
|
-
return false;
|
|
313
|
-
}
|
|
314
|
-
logger.info({ userId, calendarId }, 'Calendar selected');
|
|
315
|
-
return true;
|
|
316
|
-
}
|
|
317
|
-
catch (error) {
|
|
318
|
-
logger.error({ error, userId }, 'Failed to select calendar');
|
|
319
|
-
return false;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
/**
|
|
323
|
-
* Set multiple calendars for conflict checking (multi-select).
|
|
324
|
-
* Also updates the primary calendar_id to the first selected.
|
|
325
|
-
*/
|
|
326
|
-
async selectCalendars(userId, calendarIds) {
|
|
327
|
-
try {
|
|
328
|
-
const db = await this.getSystemDB();
|
|
329
|
-
const primaryId = calendarIds[0] || 'primary';
|
|
330
|
-
const { error } = await db
|
|
331
|
-
.from('planner_calendar_tokens')
|
|
332
|
-
.update({
|
|
333
|
-
calendar_id: primaryId,
|
|
334
|
-
selected_calendar_ids: calendarIds,
|
|
335
|
-
updated_at: new Date().toISOString(),
|
|
336
|
-
})
|
|
337
|
-
.eq('user_id', userId)
|
|
338
|
-
.eq('is_active', true);
|
|
339
|
-
if (error) {
|
|
340
|
-
logger.error({ error, userId, calendarIds }, 'Failed to select calendars');
|
|
341
|
-
return false;
|
|
342
|
-
}
|
|
343
|
-
logger.info({ userId, calendarIds, count: calendarIds.length }, 'Calendars selected');
|
|
344
|
-
return true;
|
|
345
|
-
}
|
|
346
|
-
catch (error) {
|
|
347
|
-
logger.error({ error, userId }, 'Failed to select calendars');
|
|
348
|
-
return false;
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
/**
|
|
352
|
-
* Check if a user has an active Google Calendar connection.
|
|
353
|
-
*/
|
|
354
|
-
async isConnected(userId) {
|
|
355
|
-
try {
|
|
356
|
-
const db = await this.getSystemDB();
|
|
357
|
-
const { data, error } = await db
|
|
358
|
-
.from('planner_calendar_tokens')
|
|
359
|
-
.select('id')
|
|
360
|
-
.eq('user_id', userId)
|
|
361
|
-
.eq('is_active', true)
|
|
362
|
-
.single();
|
|
363
|
-
return !error && !!data;
|
|
364
|
-
}
|
|
365
|
-
catch {
|
|
366
|
-
return false;
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
/**
|
|
370
|
-
* Disconnect Google Calendar for a user.
|
|
371
|
-
*/
|
|
372
|
-
async disconnect(userId) {
|
|
373
|
-
const db = await this.getSystemDB();
|
|
374
|
-
await db
|
|
375
|
-
.from('planner_calendar_tokens')
|
|
376
|
-
.update({ is_active: false, updated_at: new Date().toISOString() })
|
|
377
|
-
.eq('user_id', userId);
|
|
378
|
-
logger.info({ userId }, 'Calendar disconnected');
|
|
379
|
-
}
|
|
380
|
-
/**
|
|
381
|
-
* Fetch events from ALL selected Google Calendars for a date range.
|
|
382
|
-
* Returns busy time blocks merged across all selected calendars.
|
|
383
|
-
*
|
|
384
|
-
* This replaces the legacy single-calendar getBusySlots behaviour:
|
|
385
|
-
* if the user has multiple calendars selected via selected_calendar_ids,
|
|
386
|
-
* busy events from all of them are returned.
|
|
387
|
-
*/
|
|
388
|
-
async getBusySlots(userId, dateStart, dateEnd) {
|
|
389
|
-
try {
|
|
390
|
-
const oauth2Client = await this.getAuthenticatedClient(userId);
|
|
391
|
-
if (!oauth2Client)
|
|
392
|
-
return [];
|
|
393
|
-
const google = await this.getGoogle();
|
|
394
|
-
const calendar = google.calendar({ version: 'v3', auth: oauth2Client });
|
|
395
|
-
// Get all selected calendar IDs (multi-select)
|
|
396
|
-
const calendarIds = await this.getSelectedCalendarIds(userId);
|
|
397
|
-
// Get calendar metadata for display names (best-effort)
|
|
398
|
-
let calMeta = new Map();
|
|
399
|
-
try {
|
|
400
|
-
const calList = await calendar.calendarList.list({ minAccessRole: 'reader' });
|
|
401
|
-
for (const cal of calList.data.items || []) {
|
|
402
|
-
if (cal.id)
|
|
403
|
-
calMeta.set(cal.id, cal.summary || cal.id);
|
|
404
|
-
if (cal.primary)
|
|
405
|
-
calMeta.set('primary', cal.summary || 'Primary');
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
catch {
|
|
409
|
-
/* non-fatal */
|
|
410
|
-
}
|
|
411
|
-
// Fetch events from each calendar in parallel
|
|
412
|
-
const allEvents = [];
|
|
413
|
-
const results = await Promise.allSettled(calendarIds.map((calId) => calendar.events.list({
|
|
414
|
-
calendarId: calId,
|
|
415
|
-
timeMin: `${dateStart}T00:00:00Z`,
|
|
416
|
-
timeMax: `${dateEnd}T23:59:59.999Z`,
|
|
417
|
-
singleEvents: true,
|
|
418
|
-
orderBy: 'startTime',
|
|
419
|
-
showDeleted: false,
|
|
420
|
-
})));
|
|
421
|
-
for (let i = 0; i < results.length; i++) {
|
|
422
|
-
const result = results[i];
|
|
423
|
-
const calId = calendarIds[i];
|
|
424
|
-
const calName = calMeta.get(calId) || calId;
|
|
425
|
-
if (result.status === 'rejected') {
|
|
426
|
-
logger.warn({ userId, calId, error: result.reason }, 'Failed to fetch events for calendar');
|
|
427
|
-
continue;
|
|
428
|
-
}
|
|
429
|
-
const events = result.value.data.items || [];
|
|
430
|
-
for (const e of events) {
|
|
431
|
-
if (e.status === 'cancelled')
|
|
432
|
-
continue;
|
|
433
|
-
// Skip events the user said they're free for
|
|
434
|
-
if (e.transparency === 'transparent')
|
|
435
|
-
continue;
|
|
436
|
-
// Timed events
|
|
437
|
-
if (e.start?.dateTime && e.end?.dateTime) {
|
|
438
|
-
allEvents.push({
|
|
439
|
-
start_time: e.start.dateTime,
|
|
440
|
-
end_time: e.end.dateTime,
|
|
441
|
-
summary: e.summary,
|
|
442
|
-
calendarId: calId,
|
|
443
|
-
calendarName: calName,
|
|
444
|
-
});
|
|
445
|
-
continue;
|
|
446
|
-
}
|
|
447
|
-
// All-day events: convert date → full-day busy block
|
|
448
|
-
if (e.start?.date && e.end?.date) {
|
|
449
|
-
allEvents.push({
|
|
450
|
-
start_time: `${e.start.date}T00:00:00Z`,
|
|
451
|
-
end_time: `${e.end.date}T00:00:00Z`,
|
|
452
|
-
summary: e.summary,
|
|
453
|
-
calendarId: calId,
|
|
454
|
-
calendarName: calName,
|
|
455
|
-
});
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
logger.debug({ userId, calendarCount: calendarIds.length, eventCount: allEvents.length }, 'Fetched busy slots');
|
|
460
|
-
return allEvents;
|
|
461
|
-
}
|
|
462
|
-
catch (error) {
|
|
463
|
-
logger.error({ error, userId }, 'Failed to fetch busy slots from Google Calendar');
|
|
464
|
-
return [];
|
|
465
|
-
}
|
|
466
|
-
}
|
|
467
|
-
/**
|
|
468
|
-
* Register a Google Calendar push notification channel for real-time event updates.
|
|
469
|
-
* Returns the channel info that should be stored for later teardown.
|
|
470
|
-
* Requires a publicly accessible HTTPS webhook URL.
|
|
471
|
-
*
|
|
472
|
-
* Note: Google Calendar push channels expire after 1 hour to 30 days depending on type.
|
|
473
|
-
* Caller is responsible for periodic renewal.
|
|
474
|
-
*/
|
|
475
|
-
async watchCalendar(userId, calendarId, webhookUrl, channelId, expirationMs) {
|
|
476
|
-
try {
|
|
477
|
-
const oauth2Client = await this.getAuthenticatedClient(userId);
|
|
478
|
-
if (!oauth2Client)
|
|
479
|
-
return null;
|
|
480
|
-
const google = await this.getGoogle();
|
|
481
|
-
const calendar = google.calendar({ version: 'v3', auth: oauth2Client });
|
|
482
|
-
const response = await calendar.events.watch({
|
|
483
|
-
calendarId,
|
|
484
|
-
requestBody: {
|
|
485
|
-
id: channelId,
|
|
486
|
-
type: 'web_hook',
|
|
487
|
-
address: webhookUrl,
|
|
488
|
-
...(expirationMs ? { expiration: String(Date.now() + expirationMs) } : {}),
|
|
489
|
-
},
|
|
490
|
-
});
|
|
491
|
-
logger.info({ userId, calendarId, channelId, resourceId: response.data.resourceId }, 'Calendar watch channel created');
|
|
492
|
-
return {
|
|
493
|
-
channelId: response.data.id || channelId,
|
|
494
|
-
resourceId: response.data.resourceId || '',
|
|
495
|
-
expiration: response.data.expiration || '',
|
|
496
|
-
};
|
|
497
|
-
}
|
|
498
|
-
catch (error) {
|
|
499
|
-
logger.error({ error, userId, calendarId }, 'Failed to register calendar watch channel');
|
|
500
|
-
return null;
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
/**
|
|
504
|
-
* Stop a previously registered push notification channel.
|
|
505
|
-
*/
|
|
506
|
-
async stopWatch(userId, channelId, resourceId) {
|
|
507
|
-
try {
|
|
508
|
-
const oauth2Client = await this.getAuthenticatedClient(userId);
|
|
509
|
-
if (!oauth2Client)
|
|
510
|
-
return false;
|
|
511
|
-
const google = await this.getGoogle();
|
|
512
|
-
const calendar = google.calendar({ version: 'v3', auth: oauth2Client });
|
|
513
|
-
await calendar.channels.stop({
|
|
514
|
-
requestBody: { id: channelId, resourceId },
|
|
515
|
-
});
|
|
516
|
-
logger.info({ userId, channelId }, 'Calendar watch channel stopped');
|
|
517
|
-
return true;
|
|
518
|
-
}
|
|
519
|
-
catch (error) {
|
|
520
|
-
logger.error({ error, userId, channelId }, 'Failed to stop calendar watch channel');
|
|
521
|
-
return false;
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
//# sourceMappingURL=GoogleCalendarService.js.map
|