import { Inject, Injectable, Logger } from '@nestjs/common'; import type { CalendarAccount } from '@prisma/client'; import { CalendarProviderRequestError, Config, OnEvent } from '../../../base'; import { CalendarProviderFactory, CalendarProviderName } from './factory'; export interface CalendarProviderTokens { accessToken: string; refreshToken?: string; expiresAt?: Date; scope?: string; tokenType?: string; } export interface CalendarAccountProfile { providerAccountId: string; displayName?: string; email?: string; } export interface CalendarProviderCalendar { id: string; summary?: string; timeZone?: string; colorId?: string; primary?: boolean; } export interface CalendarProviderEventTime { dateTime?: string; date?: string; timeZone?: string; } export interface CalendarProviderEvent { id: string; status?: string; etag?: string; summary?: string; description?: string; location?: string; updated?: string; recurringEventId?: string; originalStartTime?: CalendarProviderEventTime; start: CalendarProviderEventTime; end: CalendarProviderEventTime; raw: Record; } export interface CalendarProviderListEventsParams { accessToken: string; calendarId: string; account?: CalendarAccount; timeMin?: string; timeMax?: string; syncToken?: string; } export interface CalendarProviderListCalendarsParams { accessToken: string; account?: CalendarAccount; } export interface CalendarProviderListEventsResult { events: CalendarProviderEvent[]; nextSyncToken?: string; } export interface CalendarProviderWatchResult { channelId: string; resourceId: string; expiration?: Date; } export interface CalendarProviderWatchParams { accessToken: string; calendarId: string; address: string; token?: string; channelId: string; } export interface CalendarProviderStopParams { accessToken: string; channelId: string; resourceId: string; } @Injectable() export abstract class CalendarProvider { abstract provider: CalendarProviderName; abstract getAuthUrl(state: string, redirectUri: string): string; abstract exchangeCode( code: string, redirectUri: string ): Promise; abstract refreshTokens(refreshToken: string): Promise; abstract getAccountProfile( accessToken: string ): Promise; abstract listCalendars( params: CalendarProviderListCalendarsParams ): Promise; abstract listEvents( params: CalendarProviderListEventsParams ): Promise; abstract watchCalendar?( params: CalendarProviderWatchParams ): Promise; abstract stopChannel?(params: CalendarProviderStopParams): Promise; protected readonly logger = new Logger(this.constructor.name); @Inject() private readonly factory!: CalendarProviderFactory; @Inject() private readonly AFFiNEConfig!: Config; get config() { return (this.AFFiNEConfig.calendar as Record)[this.provider]; } get configured() { if (!this.config || !this.config.enabled) { return false; } if ('clientId' in this.config || 'clientSecret' in this.config) { return Boolean(this.config.clientId && this.config.clientSecret); } return true; } get supportsOAuth() { return true; } @OnEvent('config.init') onConfigInit() { this.setup(); } @OnEvent('config.changed') onConfigUpdated(event: Events['config.changed']) { if ('calendar' in event.updates) { this.setup(); } } protected setup() { if (this.configured) { this.factory.register(this); } else { this.factory.unregister(this); } } protected get requestTimeoutMs() { const timeout = (this.config as { requestTimeoutMs?: number } | undefined) ?.requestTimeoutMs; return typeof timeout === 'number' && timeout > 0 ? timeout : undefined; } protected withTimeout(signal?: AbortSignal | null) { const timeoutMs = this.requestTimeoutMs; if (!timeoutMs) return signal; const timeoutSignal = AbortSignal.timeout(timeoutMs); if (!signal) return timeoutSignal; return AbortSignal.any([signal, timeoutSignal]); } protected async fetchJson(url: string, init?: RequestInit) { const response = await fetch(url, { ...init, signal: this.withTimeout(init?.signal), headers: { ...init?.headers, Accept: 'application/json' }, }); const body = await response.text(); if (!response.ok) { throw new CalendarProviderRequestError({ status: response.status, message: body, }); } if (!body) { return {} as T; } return JSON.parse(body) as T; } protected postFormJson( url: string, body: string, options?: { headers?: Record } ) { return this.fetchJson(url, { method: 'POST', body, headers: { 'Content-Type': 'application/x-www-form-urlencoded', ...options?.headers, }, }); } }