feat: api v2 get google-calendar event details (#21336)
* feat: add API endpoint to fetch Google Calendar meeting details and metrics * refactor: rename eventId to eventUid and update calendar event details endpoint * feat: implement Google Calendar service account delegation for event details * refactor: simplify Google Calendar event response * refactor: simplify calendar event details endpoint * feat: add calendar event details endpoint with schema and controller implementation * chore: reset platform libraries package version to 0.0.0 * refactor: migrate calendar event details to unified calendars module * Update gcal.service.ts * refactor: update calendar event schema with title and locations fields replacing summary and conferenceData * feat: update calendar event source to use enum type for better validation * update api description * refactor: make Google Calendar service methods private * refactor: remove location field from unified calendar event output * fix: update Google Calendar event location type property to match unified schema * feat: add CalendarEventStatus enum and map Google Calendar statuses to standardized values * docs: add property descriptions to calendar event schema and remove organizer field * fix: change calendar event locations type from null to empty array in unified calendar API * feat: make calendar source field required * refactor: replace organizer field with hosts array in Google calendar event mapping * feat: enhance calendar event location API documentation with detailed property descriptions * feat: add calendar event details endpoint documentation * fix: typo * docs: update calendar event details API description to remove reschedule history reference * refactor: rename uri to url in calendar event location interfaces and mappings * refactor: improve calendar event response status handling and code organization * Update get-unified-calendar-event.ts * update docs * chore: update @calcom/platform-libraries to version 0.0.215
This commit is contained in:
@@ -37,7 +37,7 @@
|
||||
"@axiomhq/winston": "^1.2.0",
|
||||
"@calcom/platform-constants": "*",
|
||||
"@calcom/platform-enums": "*",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.214",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.215",
|
||||
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/platform-utils": "*",
|
||||
|
||||
@@ -24,4 +24,26 @@ export class BookingReferencesRepository_2024_08_13 {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getBookingReferencesIncludeSensitiveCredentials(eventUid: string) {
|
||||
return this.dbRead.prisma.bookingReference.findFirst({
|
||||
where: {
|
||||
uid: eventUid,
|
||||
},
|
||||
include: {
|
||||
credential: true,
|
||||
delegationCredential: true,
|
||||
booking: {
|
||||
select: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/booking-references.repository";
|
||||
import { BookingsRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/bookings.repository";
|
||||
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
|
||||
import { CalendarsController } from "@/ee/calendars/controllers/calendars.controller";
|
||||
import { AppleCalendarService } from "@/ee/calendars/services/apple-calendar.service";
|
||||
@@ -25,8 +27,10 @@ import { Module } from "@nestjs/common";
|
||||
SelectedCalendarsRepository,
|
||||
AppsRepository,
|
||||
CalendarsRepository,
|
||||
BookingsRepository_2024_08_13,
|
||||
BookingReferencesRepository_2024_08_13,
|
||||
],
|
||||
controllers: [CalendarsController],
|
||||
exports: [CalendarsService],
|
||||
exports: [CalendarsService, GoogleCalendarService],
|
||||
})
|
||||
export class CalendarsModule {}
|
||||
|
||||
@@ -23,7 +23,7 @@ const CALENDAR_SCOPES = [
|
||||
|
||||
@Injectable()
|
||||
export class GoogleCalendarService implements OAuthCalendarApp {
|
||||
private redirectUri = `${this.config.get("api.url")}/gcal/oauth/save`;
|
||||
public readonly redirectUri = `${this.config.get("api.url")}/gcal/oauth/save`;
|
||||
private gcalResponseSchema = z.object({ client_id: z.string(), client_secret: z.string() });
|
||||
private logger = new Logger("GcalService");
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/booking-references.repository";
|
||||
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
|
||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||
import { GoogleCalendarService as GCalService } from "@/ee/calendars/services/gcal.service";
|
||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||
import { CalUnifiedCalendarsController } from "@/modules/cal-unified-calendars/controllers/cal-unified-calendars.controller";
|
||||
import { GoogleCalendarService } from "@/modules/cal-unified-calendars/services/google-calendar.service";
|
||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [],
|
||||
providers: [
|
||||
GCalService,
|
||||
GoogleCalendarService,
|
||||
AppsRepository,
|
||||
BookingReferencesRepository_2024_08_13,
|
||||
CredentialsRepository,
|
||||
CalendarsService,
|
||||
TokensRepository,
|
||||
SelectedCalendarsRepository,
|
||||
PrismaReadService,
|
||||
PrismaWriteService,
|
||||
UsersRepository,
|
||||
CalendarsRepository,
|
||||
],
|
||||
controllers: [CalUnifiedCalendarsController],
|
||||
exports: [GoogleCalendarService],
|
||||
})
|
||||
export class CalUnifiedCalendarsModule {}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { GetUnifiedCalendarEventOutput } from "@/modules/cal-unified-calendars/outputs/get-unified-calendar-event";
|
||||
import { GoogleCalendarEventOutputPipe } from "@/modules/cal-unified-calendars/pipes/get-calendar-event-details-output-pipe";
|
||||
import { GoogleCalendarService } from "@/modules/cal-unified-calendars/services/google-calendar.service";
|
||||
import { Controller, Get, Param, UseGuards, HttpCode, HttpStatus, BadRequestException } from "@nestjs/common";
|
||||
import { ApiTags as DocsTags, ApiParam, ApiHeader, ApiOperation } from "@nestjs/swagger";
|
||||
|
||||
import { GOOGLE_CALENDAR, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
@Controller({
|
||||
path: "/v2/calendars",
|
||||
version: API_VERSIONS_VALUES,
|
||||
})
|
||||
@DocsTags("Cal Unified Calendars")
|
||||
export class CalUnifiedCalendarsController {
|
||||
constructor(private readonly googleCalendarService: GoogleCalendarService) {}
|
||||
|
||||
@ApiParam({
|
||||
name: "calendar",
|
||||
enum: [GOOGLE_CALENDAR],
|
||||
type: String,
|
||||
})
|
||||
@ApiParam({
|
||||
name: "eventUid",
|
||||
description: "The Google Calendar event ID",
|
||||
type: String,
|
||||
})
|
||||
@Get("/:calendar/event/:eventUid")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UseGuards(ApiAuthGuard, PermissionsGuard)
|
||||
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
|
||||
@ApiOperation({
|
||||
summary: "Get meeting Details from calendar",
|
||||
description: "Returns detailed information about a meeting including attendance metrics",
|
||||
})
|
||||
async getCalendarEventDetails(
|
||||
@Param("calendar") calendar: string,
|
||||
@Param("eventUid") eventUid: string
|
||||
): Promise<GetUnifiedCalendarEventOutput> {
|
||||
if (calendar !== GOOGLE_CALENDAR) {
|
||||
throw new BadRequestException("Meeting details are currently only available for Google Calendar");
|
||||
}
|
||||
|
||||
const eventDetails = await this.googleCalendarService.getEventDetails(eventUid);
|
||||
|
||||
const transformedEvent = new GoogleCalendarEventOutputPipe().transform(eventDetails);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: transformedEvent,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,395 @@
|
||||
import { ApiProperty, ApiPropertyOptional, getSchemaPath } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsEnum, IsISO8601, IsOptional, IsString, ValidateNested } from "class-validator";
|
||||
|
||||
import { CALENDARS, SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export enum CalendarEventStatus {
|
||||
ACCEPTED = "accepted",
|
||||
PENDING = "pending",
|
||||
DECLINED = "declined",
|
||||
CANCELLED = "cancelled",
|
||||
}
|
||||
|
||||
export enum CalendarEventResponseStatus {
|
||||
ACCEPTED = "accepted",
|
||||
PENDING = "pending",
|
||||
DECLINED = "declined",
|
||||
NEEDS_ACTION = "needsAction",
|
||||
}
|
||||
|
||||
/**
|
||||
* Base interface for all calendar event locations
|
||||
*/
|
||||
export interface ICalendarEventLocation {
|
||||
type: string;
|
||||
url: string;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export class CalendarEventVideoLocation implements ICalendarEventLocation {
|
||||
@IsString()
|
||||
@ApiProperty({
|
||||
enum: ["video"],
|
||||
description: "Indicates this is a video conference location",
|
||||
})
|
||||
type = "video";
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({
|
||||
description: "URL for joining the video conference",
|
||||
})
|
||||
url!: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
nullable: true,
|
||||
description: "Display name for the video conference",
|
||||
})
|
||||
label?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
nullable: true,
|
||||
description: "Password required to join the video conference",
|
||||
})
|
||||
password?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
nullable: true,
|
||||
description: "Meeting code or ID required to join the conference",
|
||||
})
|
||||
meetingCode?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
nullable: true,
|
||||
description: "Access code required to join the conference",
|
||||
})
|
||||
accessCode?: string;
|
||||
}
|
||||
|
||||
export class CalendarEventPhoneLocation implements ICalendarEventLocation {
|
||||
@IsString()
|
||||
@ApiProperty({
|
||||
enum: ["phone"],
|
||||
description: "Indicates this is a phone conference location",
|
||||
})
|
||||
type = "phone";
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({
|
||||
description: "Phone number or URL for dialing into the conference",
|
||||
})
|
||||
url!: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
nullable: true,
|
||||
description: "Display name for the phone conference",
|
||||
})
|
||||
label?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
nullable: true,
|
||||
description: "PIN number required for the phone conference",
|
||||
})
|
||||
pin?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
nullable: true,
|
||||
description: "Password required for the phone conference",
|
||||
})
|
||||
password?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
nullable: true,
|
||||
description: "Access code required for the phone conference",
|
||||
})
|
||||
accessCode?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
nullable: true,
|
||||
description: "Country/region code for the phone number",
|
||||
})
|
||||
regionCode?: string;
|
||||
}
|
||||
|
||||
export class CalendarEventSipLocation implements ICalendarEventLocation {
|
||||
@IsString()
|
||||
@ApiProperty({
|
||||
enum: ["sip"],
|
||||
description: "Indicates this is a SIP (Session Initiation Protocol) conference location",
|
||||
})
|
||||
type = "sip";
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({
|
||||
description: "SIP URL for joining the conference",
|
||||
})
|
||||
url!: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
nullable: true,
|
||||
description: "Display name for the SIP conference",
|
||||
})
|
||||
label?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
nullable: true,
|
||||
description: "PIN number required for the SIP conference",
|
||||
})
|
||||
pin?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
nullable: true,
|
||||
description: "Password required for the SIP conference",
|
||||
})
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export class CalendarEventMoreLocation implements ICalendarEventLocation {
|
||||
@IsString()
|
||||
@ApiProperty({
|
||||
enum: ["more"],
|
||||
description: "Indicates this is an additional conference location type",
|
||||
})
|
||||
type = "more";
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({
|
||||
description: "URL for accessing this location",
|
||||
})
|
||||
url!: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
nullable: true,
|
||||
description: "Display name for this location",
|
||||
})
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export type CalendarEventLocation =
|
||||
| CalendarEventVideoLocation
|
||||
| CalendarEventPhoneLocation
|
||||
| CalendarEventSipLocation
|
||||
| CalendarEventMoreLocation;
|
||||
|
||||
export class CalendarEventHost {
|
||||
@IsString()
|
||||
@ApiProperty({
|
||||
description: "Email address of the event host",
|
||||
})
|
||||
email!: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
nullable: true,
|
||||
description: "Display name of the event host",
|
||||
})
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export class CalendarEventAttendee {
|
||||
@IsString()
|
||||
@ApiProperty({
|
||||
description: "Email address of the attendee",
|
||||
})
|
||||
email!: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
description: "Display name of the attendee",
|
||||
})
|
||||
name?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@IsEnum(CalendarEventResponseStatus)
|
||||
@ApiPropertyOptional({
|
||||
enum: CalendarEventResponseStatus,
|
||||
nullable: true,
|
||||
enumName: "CalendarEventResponseStatus",
|
||||
description: "Attendee's response to the invitation",
|
||||
example: CalendarEventResponseStatus.ACCEPTED,
|
||||
})
|
||||
responseStatus!: CalendarEventResponseStatus | null;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
nullable: true,
|
||||
description: "Indicates if this attendee is the current user",
|
||||
})
|
||||
self?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
nullable: true,
|
||||
description: "Indicates if this attendee's attendance is optional",
|
||||
})
|
||||
optional?: boolean;
|
||||
}
|
||||
|
||||
export class DateTimeWithZone {
|
||||
@IsISO8601()
|
||||
time!: string;
|
||||
|
||||
@IsString()
|
||||
timeZone!: string;
|
||||
}
|
||||
|
||||
export class UnifiedCalendarEventOutput {
|
||||
@ValidateNested()
|
||||
@Type(() => DateTimeWithZone)
|
||||
@ApiProperty({
|
||||
type: "object",
|
||||
properties: {
|
||||
time: { type: "string", format: "date-time" },
|
||||
timeZone: { type: "string" },
|
||||
},
|
||||
description: "Start date and time of the calendar event with timezone information",
|
||||
})
|
||||
start!: DateTimeWithZone;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => DateTimeWithZone)
|
||||
@ApiProperty({
|
||||
type: "object",
|
||||
properties: {
|
||||
time: { type: "string", format: "date-time" },
|
||||
timeZone: { type: "string" },
|
||||
},
|
||||
description: "End date and time of the calendar event with timezone information",
|
||||
})
|
||||
end!: DateTimeWithZone;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({
|
||||
type: String,
|
||||
description: "Unique identifier of the calendar event",
|
||||
})
|
||||
id!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({
|
||||
type: String,
|
||||
description: "Title of the calendar event",
|
||||
})
|
||||
title!: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
type: String,
|
||||
nullable: true,
|
||||
description: "Detailed description of the calendar event",
|
||||
})
|
||||
description?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Object, {
|
||||
discriminator: {
|
||||
property: "type",
|
||||
subTypes: [
|
||||
{ value: CalendarEventVideoLocation, name: "video" },
|
||||
{ value: CalendarEventPhoneLocation, name: "phone" },
|
||||
{ value: CalendarEventSipLocation, name: "sip" },
|
||||
{ value: CalendarEventMoreLocation, name: "more" },
|
||||
],
|
||||
},
|
||||
})
|
||||
@ApiPropertyOptional({
|
||||
type: "array",
|
||||
items: {
|
||||
oneOf: [
|
||||
{ $ref: getSchemaPath(CalendarEventVideoLocation) },
|
||||
{ $ref: getSchemaPath(CalendarEventPhoneLocation) },
|
||||
{ $ref: getSchemaPath(CalendarEventSipLocation) },
|
||||
{ $ref: getSchemaPath(CalendarEventMoreLocation) },
|
||||
],
|
||||
discriminator: {
|
||||
propertyName: "type",
|
||||
},
|
||||
},
|
||||
nullable: true,
|
||||
description: "Conference locations with entry points (video, phone, sip, more)",
|
||||
})
|
||||
locations?: CalendarEventLocation[];
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => CalendarEventAttendee)
|
||||
@ApiPropertyOptional({
|
||||
type: [CalendarEventAttendee],
|
||||
nullable: true,
|
||||
description: "List of attendees with their response status",
|
||||
})
|
||||
attendees?: CalendarEventAttendee[];
|
||||
|
||||
@IsEnum(CalendarEventStatus)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
enum: CalendarEventStatus,
|
||||
enumName: "CalendarEventStatus",
|
||||
nullable: true,
|
||||
description: "Status of the event (accepted, pending, declined, cancelled)",
|
||||
example: CalendarEventStatus.ACCEPTED,
|
||||
})
|
||||
status?: CalendarEventStatus | null;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => CalendarEventHost)
|
||||
@ApiPropertyOptional({
|
||||
type: [CalendarEventHost],
|
||||
nullable: true,
|
||||
description: "Information about the event hosts (organizers)",
|
||||
})
|
||||
hosts?: CalendarEventHost[];
|
||||
|
||||
@IsEnum(CALENDARS)
|
||||
@ApiProperty({
|
||||
enum: CALENDARS,
|
||||
enumName: "CalendarSource",
|
||||
description:
|
||||
"Calendar integration source (e.g., Google Calendar, Office 365, Apple Calendar). Currently only Google Calendar is supported.",
|
||||
example: "google",
|
||||
})
|
||||
source!: (typeof CALENDARS)[number];
|
||||
}
|
||||
|
||||
export class GetUnifiedCalendarEventOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => UnifiedCalendarEventOutput)
|
||||
@ApiProperty({ type: UnifiedCalendarEventOutput })
|
||||
data!: UnifiedCalendarEventOutput;
|
||||
}
|
||||
+205
@@ -0,0 +1,205 @@
|
||||
import {
|
||||
CalendarEventResponseStatus,
|
||||
CalendarEventStatus,
|
||||
DateTimeWithZone,
|
||||
UnifiedCalendarEventOutput,
|
||||
} from "@/modules/cal-unified-calendars/outputs/get-unified-calendar-event";
|
||||
import { PipeTransform, Injectable } from "@nestjs/common";
|
||||
|
||||
export interface GoogleCalendarEventResponse {
|
||||
kind: string;
|
||||
etag: string;
|
||||
id: string;
|
||||
status: string;
|
||||
htmlLink: string;
|
||||
created: string;
|
||||
updated: string;
|
||||
summary: string;
|
||||
description?: string;
|
||||
location?: string;
|
||||
creator: {
|
||||
email: string;
|
||||
displayName?: string;
|
||||
};
|
||||
organizer: {
|
||||
email: string;
|
||||
displayName?: string;
|
||||
self?: boolean;
|
||||
};
|
||||
start: {
|
||||
dateTime: string;
|
||||
timeZone: string;
|
||||
};
|
||||
end: {
|
||||
dateTime: string;
|
||||
timeZone: string;
|
||||
};
|
||||
iCalUID: string;
|
||||
sequence: number;
|
||||
attendees?: Array<{
|
||||
email: string;
|
||||
displayName?: string;
|
||||
organizer?: boolean;
|
||||
self?: boolean;
|
||||
responseStatus?: string;
|
||||
optional?: boolean;
|
||||
}>;
|
||||
hangoutLink?: string;
|
||||
conferenceData?: {
|
||||
createRequest?: {
|
||||
requestId: string;
|
||||
conferenceSolutionKey: {
|
||||
type: string;
|
||||
};
|
||||
status: {
|
||||
statusCode: string;
|
||||
};
|
||||
};
|
||||
entryPoints?: Array<{
|
||||
entryPointType: string;
|
||||
uri: string;
|
||||
label?: string;
|
||||
pin?: string;
|
||||
regionCode?: string;
|
||||
}>;
|
||||
conferenceSolution?: {
|
||||
key: {
|
||||
type: string;
|
||||
};
|
||||
name: string;
|
||||
iconUri: string;
|
||||
};
|
||||
conferenceId: string;
|
||||
};
|
||||
reminders?: {
|
||||
useDefault: boolean;
|
||||
};
|
||||
eventType?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class GoogleCalendarEventOutputPipe
|
||||
implements PipeTransform<GoogleCalendarEventResponse, UnifiedCalendarEventOutput>
|
||||
{
|
||||
transform(googleEvent: GoogleCalendarEventResponse): UnifiedCalendarEventOutput {
|
||||
const calendarEvent = new UnifiedCalendarEventOutput();
|
||||
|
||||
calendarEvent.id = googleEvent.id;
|
||||
calendarEvent.title = googleEvent.summary;
|
||||
calendarEvent.description = googleEvent.description || null;
|
||||
|
||||
calendarEvent.start = this.transformDateTimeWithZone(googleEvent.start);
|
||||
calendarEvent.end = this.transformDateTimeWithZone(googleEvent.end);
|
||||
|
||||
calendarEvent.locations = this.transformLocations(googleEvent);
|
||||
|
||||
if (googleEvent.attendees && googleEvent.attendees.length > 0) {
|
||||
calendarEvent.attendees = googleEvent.attendees
|
||||
.filter((attendee) => !attendee.organizer)
|
||||
.map((attendee) => {
|
||||
return {
|
||||
email: attendee.email,
|
||||
name: attendee.displayName,
|
||||
responseStatus: this.transformAttendeeResponseStatus(attendee.responseStatus),
|
||||
organizer: attendee.organizer,
|
||||
self: attendee.self,
|
||||
optional: attendee.optional,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
calendarEvent.status = this.transformEventStatus(googleEvent.status);
|
||||
|
||||
calendarEvent.hosts = this.transformHosts(googleEvent);
|
||||
|
||||
calendarEvent.source = "google";
|
||||
|
||||
return calendarEvent;
|
||||
}
|
||||
|
||||
private transformDateTimeWithZone(googleDateTime: {
|
||||
dateTime: string;
|
||||
timeZone: string;
|
||||
}): DateTimeWithZone {
|
||||
const dateTimeWithZone = new DateTimeWithZone();
|
||||
dateTimeWithZone.time = googleDateTime.dateTime;
|
||||
dateTimeWithZone.timeZone = googleDateTime.timeZone;
|
||||
return dateTimeWithZone;
|
||||
}
|
||||
|
||||
private transformAttendeeResponseStatus(responseStatus?: string): CalendarEventResponseStatus | null {
|
||||
if (!responseStatus) return null;
|
||||
|
||||
switch (responseStatus.toLowerCase()) {
|
||||
case "accepted":
|
||||
return CalendarEventResponseStatus.ACCEPTED;
|
||||
case "tentative":
|
||||
return CalendarEventResponseStatus.PENDING;
|
||||
case "declined":
|
||||
return CalendarEventResponseStatus.DECLINED;
|
||||
case "needsaction":
|
||||
return CalendarEventResponseStatus.NEEDS_ACTION;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private transformEventStatus(eventStatus?: string): CalendarEventStatus | null {
|
||||
if (!eventStatus) return null;
|
||||
|
||||
switch (eventStatus.toLowerCase()) {
|
||||
case "confirmed":
|
||||
return CalendarEventStatus.ACCEPTED;
|
||||
case "tentative":
|
||||
return CalendarEventStatus.PENDING;
|
||||
case "cancelled":
|
||||
return CalendarEventStatus.CANCELLED;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private transformHosts(googleEvent: GoogleCalendarEventResponse): Array<{ email: string; name?: string }> {
|
||||
if (googleEvent.organizer) {
|
||||
return [
|
||||
{
|
||||
email: googleEvent.organizer.email,
|
||||
name: googleEvent.organizer.displayName,
|
||||
},
|
||||
];
|
||||
} else if (googleEvent.attendees) {
|
||||
// If no explicit organizer, find the first attendee with organizer flag
|
||||
const organizerAttendee = googleEvent.attendees.find((attendee) => attendee.organizer);
|
||||
if (organizerAttendee) {
|
||||
return [
|
||||
{
|
||||
email: organizerAttendee.email,
|
||||
name: organizerAttendee.displayName,
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
private transformLocations(
|
||||
googleEvent: GoogleCalendarEventResponse
|
||||
): Array<{ type: string; url: string; label?: string; pin?: string; regionCode?: string }> {
|
||||
if (googleEvent?.conferenceData?.entryPoints) {
|
||||
return googleEvent.conferenceData.entryPoints.map((entryPoint) => {
|
||||
return {
|
||||
type: entryPoint.entryPointType,
|
||||
url: entryPoint.uri,
|
||||
label: entryPoint.label,
|
||||
pin: entryPoint.pin,
|
||||
regionCode: entryPoint.regionCode,
|
||||
};
|
||||
});
|
||||
} else if (googleEvent.hangoutLink) {
|
||||
return [{ type: "video", url: googleEvent.hangoutLink }];
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import { BookingReferencesRepository_2024_08_13 } from "@/ee/bookings/2024-08-13/booking-references.repository";
|
||||
import { GoogleCalendarService as GCalService } from "@/ee/calendars/services/gcal.service";
|
||||
import { GoogleCalendarEventResponse } from "@/modules/cal-unified-calendars/pipes/get-calendar-event-details-output-pipe";
|
||||
import { calendar_v3 } from "@googleapis/calendar";
|
||||
import { Injectable, UnauthorizedException } from "@nestjs/common";
|
||||
import { NotFoundException } from "@nestjs/common";
|
||||
import { Logger } from "@nestjs/common";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { JWT } from "googleapis-common";
|
||||
|
||||
import { DelegationCredentialRepository, OAuth2UniversalSchema } from "@calcom/platform-libraries/app-store";
|
||||
|
||||
import { UnifiedCalendarEventOutput } from "../outputs/get-unified-calendar-event";
|
||||
|
||||
@Injectable()
|
||||
export class GoogleCalendarService {
|
||||
private logger = new Logger("GoogleCalendarService");
|
||||
constructor(
|
||||
private readonly bookingReferencesRepository: BookingReferencesRepository_2024_08_13,
|
||||
private readonly gCalService: GCalService
|
||||
) {}
|
||||
|
||||
async getEventDetails(eventUid: string): Promise<GoogleCalendarEventResponse> {
|
||||
const bookingReference =
|
||||
await this.bookingReferencesRepository.getBookingReferencesIncludeSensitiveCredentials(eventUid);
|
||||
|
||||
if (!bookingReference) {
|
||||
throw new NotFoundException("Booking reference not found");
|
||||
}
|
||||
|
||||
const ownerUserEmail = bookingReference?.booking?.user?.email;
|
||||
|
||||
// Get authenticated calendar instance
|
||||
const calendar = await this.getAuthorizedCalendarInstance(
|
||||
ownerUserEmail,
|
||||
bookingReference.credential?.key,
|
||||
bookingReference.delegationCredential
|
||||
);
|
||||
|
||||
try {
|
||||
const event = await calendar.events.get({
|
||||
calendarId: bookingReference?.externalCalendarId ?? "primary",
|
||||
eventId: bookingReference?.uid,
|
||||
// fields: "id,status,created,updated,summary,description,location,creator,organizer,start,end,attendees,conferenceData"
|
||||
});
|
||||
|
||||
if (!event.data) {
|
||||
throw new NotFoundException("Meeting not found");
|
||||
}
|
||||
return event.data as GoogleCalendarEventResponse;
|
||||
} catch (error) {
|
||||
throw new NotFoundException("Failed to retrieve meeting details");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an authorized Google Calendar instance
|
||||
* Tries delegation credentials first, falls back to direct OAuth
|
||||
*/
|
||||
private async getAuthorizedCalendarInstance(
|
||||
userEmail?: string,
|
||||
oAuthCredentials?: Prisma.JsonValue | undefined,
|
||||
delegationCredential?: { id: string } | null
|
||||
): Promise<calendar_v3.Calendar> {
|
||||
if (userEmail && delegationCredential?.id) {
|
||||
const delegatedCalendar = await this.getDelegatedCalendarInstance(delegationCredential, userEmail);
|
||||
if (delegatedCalendar) {
|
||||
return delegatedCalendar;
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to direct OAuth authentication
|
||||
if (!oAuthCredentials) {
|
||||
throw new UnauthorizedException("No valid credentials available for Google Calendar");
|
||||
}
|
||||
const parsedOAuthCredentials = OAuth2UniversalSchema.parse(oAuthCredentials);
|
||||
const oAuth2Client = await this.gCalService.getOAuthClient(this.gCalService.redirectUri);
|
||||
oAuth2Client.setCredentials(parsedOAuthCredentials);
|
||||
|
||||
return new calendar_v3.Calendar({ auth: oAuth2Client });
|
||||
}
|
||||
|
||||
private async getDelegatedCalendarInstance(
|
||||
delegationCredential: { id: string },
|
||||
emailToImpersonate: string
|
||||
): Promise<calendar_v3.Calendar | null> {
|
||||
try {
|
||||
const oauthClientIdAliasRegex = /\+[a-zA-Z0-9]{25}/;
|
||||
const cleanEmail = emailToImpersonate.replace(oauthClientIdAliasRegex, "");
|
||||
|
||||
const serviceAccountCreds =
|
||||
await DelegationCredentialRepository.findByIdIncludeSensitiveServiceAccountKey({
|
||||
id: delegationCredential.id,
|
||||
});
|
||||
|
||||
if (
|
||||
!serviceAccountCreds?.serviceAccountKey?.client_email ||
|
||||
!serviceAccountCreds?.serviceAccountKey?.private_key
|
||||
) {
|
||||
this.logger.error("Missing service account credentials for delegation", {
|
||||
delegationCredentialId: delegationCredential.id,
|
||||
});
|
||||
return null;
|
||||
}
|
||||
|
||||
const authClient = new JWT({
|
||||
email: serviceAccountCreds.serviceAccountKey.client_email,
|
||||
key: serviceAccountCreds.serviceAccountKey.private_key,
|
||||
scopes: ["https://www.googleapis.com/auth/calendar"],
|
||||
subject: cleanEmail,
|
||||
});
|
||||
|
||||
await authClient.authorize();
|
||||
return new calendar_v3.Calendar({ auth: authClient });
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { PlatformEndpointsModule } from "@/ee/platform-endpoints-module";
|
||||
import { AtomsModule } from "@/modules/atoms/atoms.module";
|
||||
import { BillingModule } from "@/modules/billing/billing.module";
|
||||
import { CalUnifiedCalendarsModule } from "@/modules/cal-unified-calendars/cal-unified-calendars.module";
|
||||
import { ConferencingModule } from "@/modules/conferencing/conferencing.module";
|
||||
import { DestinationCalendarsModule } from "@/modules/destination-calendars/destination-calendars.module";
|
||||
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
|
||||
@@ -30,6 +31,7 @@ import { WebhooksModule } from "./webhooks/webhooks.module";
|
||||
AtomsModule,
|
||||
StripeModule,
|
||||
ConferencingModule,
|
||||
CalUnifiedCalendarsModule,
|
||||
OrganizationsTeamsBookingsModule,
|
||||
OrganizationsUsersBookingsModule,
|
||||
OrganizationsBookingsModule,
|
||||
|
||||
@@ -7683,6 +7683,59 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/calendars/{calendar}/event/{eventUid}": {
|
||||
"get": {
|
||||
"operationId": "CalUnifiedCalendarsController_getCalendarEventDetails",
|
||||
"summary": "Get meeting Details from calendar",
|
||||
"description": "Returns detailed information about a meeting including attendance metrics",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "calendar",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"enum": [
|
||||
"google"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "eventUid",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"description": "The Google Calendar event ID",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Authorization",
|
||||
"in": "header",
|
||||
"description": "value must be `Bearer <token>` where `<token>` is api key prefixed with cal_ or managed user access token",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GetUnifiedCalendarEventOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Cal Unified Calendars"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/calendars/ics-feed/save": {
|
||||
"post": {
|
||||
"operationId": "CalendarsController_createIcsFeed",
|
||||
@@ -25281,6 +25334,203 @@
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"CalendarEventResponseStatus": {
|
||||
"type": "string",
|
||||
"description": "Attendee's response to the invitation",
|
||||
"enum": [
|
||||
"accepted",
|
||||
"pending",
|
||||
"declined",
|
||||
"needsAction"
|
||||
]
|
||||
},
|
||||
"CalendarEventAttendee": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"description": "Email address of the attendee"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Display name of the attendee"
|
||||
},
|
||||
"responseStatus": {
|
||||
"nullable": true,
|
||||
"example": "accepted",
|
||||
"$ref": "#/components/schemas/CalendarEventResponseStatus"
|
||||
},
|
||||
"self": {
|
||||
"type": "boolean",
|
||||
"nullable": true,
|
||||
"description": "Indicates if this attendee is the current user"
|
||||
},
|
||||
"optional": {
|
||||
"type": "boolean",
|
||||
"nullable": true,
|
||||
"description": "Indicates if this attendee's attendance is optional"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"email"
|
||||
]
|
||||
},
|
||||
"CalendarEventStatus": {
|
||||
"type": "string",
|
||||
"description": "Status of the event (accepted, pending, declined, cancelled)",
|
||||
"enum": [
|
||||
"accepted",
|
||||
"pending",
|
||||
"declined",
|
||||
"cancelled"
|
||||
]
|
||||
},
|
||||
"CalendarEventHost": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"description": "Email address of the event host"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Display name of the event host"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"email"
|
||||
]
|
||||
},
|
||||
"CalendarSource": {
|
||||
"type": "string",
|
||||
"description": "Calendar integration source (e.g., Google Calendar, Office 365, Apple Calendar). Currently only Google Calendar is supported.",
|
||||
"enum": [
|
||||
"google",
|
||||
"office365",
|
||||
"apple"
|
||||
]
|
||||
},
|
||||
"UnifiedCalendarEventOutput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"start": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"time": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"timeZone": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "Start date and time of the calendar event with timezone information"
|
||||
},
|
||||
"end": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"time": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"timeZone": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "End date and time of the calendar event with timezone information"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Unique identifier of the calendar event"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Title of the calendar event"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Detailed description of the calendar event"
|
||||
},
|
||||
"locations": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/CalendarEventVideoLocation"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/CalendarEventPhoneLocation"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/CalendarEventSipLocation"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/CalendarEventMoreLocation"
|
||||
}
|
||||
],
|
||||
"discriminator": {
|
||||
"propertyName": "type"
|
||||
}
|
||||
},
|
||||
"nullable": true,
|
||||
"description": "Conference locations with entry points (video, phone, sip, more)"
|
||||
},
|
||||
"attendees": {
|
||||
"nullable": true,
|
||||
"description": "List of attendees with their response status",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/CalendarEventAttendee"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"nullable": true,
|
||||
"example": "accepted",
|
||||
"$ref": "#/components/schemas/CalendarEventStatus"
|
||||
},
|
||||
"hosts": {
|
||||
"nullable": true,
|
||||
"description": "Information about the event hosts (organizers)",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/CalendarEventHost"
|
||||
}
|
||||
},
|
||||
"source": {
|
||||
"example": "google",
|
||||
"$ref": "#/components/schemas/CalendarSource"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"start",
|
||||
"end",
|
||||
"id",
|
||||
"title",
|
||||
"source"
|
||||
]
|
||||
},
|
||||
"GetUnifiedCalendarEventOutput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": [
|
||||
"success",
|
||||
"error"
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/UnifiedCalendarEventOutput"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"RequestEmailVerificationInput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -7349,6 +7349,55 @@
|
||||
"tags": ["Bookings"]
|
||||
}
|
||||
},
|
||||
"/v2/calendars/{calendar}/event/{eventUid}": {
|
||||
"get": {
|
||||
"operationId": "CalUnifiedCalendarsController_getCalendarEventDetails",
|
||||
"summary": "Get meeting Details from calendar",
|
||||
"description": "Returns detailed information about a meeting including attendance metrics",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "calendar",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"enum": ["google"],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "eventUid",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"description": "The Google Calendar event ID",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Authorization",
|
||||
"in": "header",
|
||||
"description": "value must be `Bearer <token>` where `<token>` is api key prefixed with cal_ or managed user access token",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GetUnifiedCalendarEventOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": ["Cal Unified Calendars"]
|
||||
}
|
||||
},
|
||||
"/v2/calendars/ics-feed/save": {
|
||||
"post": {
|
||||
"operationId": "CalendarsController_createIcsFeed",
|
||||
@@ -23086,6 +23135,173 @@
|
||||
},
|
||||
"required": ["status", "data"]
|
||||
},
|
||||
"CalendarEventResponseStatus": {
|
||||
"type": "string",
|
||||
"description": "Attendee's response to the invitation",
|
||||
"enum": ["accepted", "pending", "declined", "needsAction"]
|
||||
},
|
||||
"CalendarEventAttendee": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"description": "Email address of the attendee"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "Display name of the attendee"
|
||||
},
|
||||
"responseStatus": {
|
||||
"nullable": true,
|
||||
"example": "accepted",
|
||||
"$ref": "#/components/schemas/CalendarEventResponseStatus"
|
||||
},
|
||||
"self": {
|
||||
"type": "boolean",
|
||||
"nullable": true,
|
||||
"description": "Indicates if this attendee is the current user"
|
||||
},
|
||||
"optional": {
|
||||
"type": "boolean",
|
||||
"nullable": true,
|
||||
"description": "Indicates if this attendee's attendance is optional"
|
||||
}
|
||||
},
|
||||
"required": ["email"]
|
||||
},
|
||||
"CalendarEventStatus": {
|
||||
"type": "string",
|
||||
"description": "Status of the event (accepted, pending, declined, cancelled)",
|
||||
"enum": ["accepted", "pending", "declined", "cancelled"]
|
||||
},
|
||||
"CalendarEventHost": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
"description": "Email address of the event host"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Display name of the event host"
|
||||
}
|
||||
},
|
||||
"required": ["email"]
|
||||
},
|
||||
"CalendarSource": {
|
||||
"type": "string",
|
||||
"description": "Calendar integration source (e.g., Google Calendar, Office 365, Apple Calendar). Currently only Google Calendar is supported.",
|
||||
"enum": ["google", "office365", "apple"]
|
||||
},
|
||||
"UnifiedCalendarEventOutput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"start": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"time": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"timeZone": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "Start date and time of the calendar event with timezone information"
|
||||
},
|
||||
"end": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"time": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"timeZone": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"description": "End date and time of the calendar event with timezone information"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "Unique identifier of the calendar event"
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"description": "Title of the calendar event"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"nullable": true,
|
||||
"description": "Detailed description of the calendar event"
|
||||
},
|
||||
"locations": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/CalendarEventVideoLocation"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/CalendarEventPhoneLocation"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/CalendarEventSipLocation"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/CalendarEventMoreLocation"
|
||||
}
|
||||
],
|
||||
"discriminator": {
|
||||
"propertyName": "type"
|
||||
}
|
||||
},
|
||||
"nullable": true,
|
||||
"description": "Conference locations with entry points (video, phone, sip, more)"
|
||||
},
|
||||
"attendees": {
|
||||
"nullable": true,
|
||||
"description": "List of attendees with their response status",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/CalendarEventAttendee"
|
||||
}
|
||||
},
|
||||
"status": {
|
||||
"nullable": true,
|
||||
"example": "accepted",
|
||||
"$ref": "#/components/schemas/CalendarEventStatus"
|
||||
},
|
||||
"hosts": {
|
||||
"nullable": true,
|
||||
"description": "Information about the event hosts (organizers)",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/CalendarEventHost"
|
||||
}
|
||||
},
|
||||
"source": {
|
||||
"example": "google",
|
||||
"$ref": "#/components/schemas/CalendarSource"
|
||||
}
|
||||
},
|
||||
"required": ["start", "end", "id", "title", "source"]
|
||||
},
|
||||
"GetUnifiedCalendarEventOutput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": ["success", "error"]
|
||||
},
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/UnifiedCalendarEventOutput"
|
||||
}
|
||||
},
|
||||
"required": ["status", "data"]
|
||||
},
|
||||
"RequestEmailVerificationInput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -43,4 +43,7 @@ export {
|
||||
CalendarAppDelegationCredentialNotSetupError,
|
||||
} from "@calcom/lib/CalendarAppError";
|
||||
|
||||
export { DelegationCredentialRepository } from "@calcom/lib/server/repository/delegationCredential";
|
||||
|
||||
export { OAuth2UniversalSchema } from "@calcom/app-store/_utils/oauth/universalSchema";
|
||||
export { getUsersCredentialsIncludeServiceAccountKey } from "@calcom/lib/server/getUsersCredentials";
|
||||
|
||||
Reference in New Issue
Block a user