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:
Somay Chauhan
2025-06-13 13:08:54 +05:30
committed by GitHub
parent e55ecff899
commit 6de6f8e697
14 changed files with 6843 additions and 50 deletions
+1 -1
View File
@@ -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 {}
@@ -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;
}
@@ -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,
+250
View File
@@ -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": {
+216
View File
@@ -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": {
+3
View File
@@ -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";
+5533 -47
View File
File diff suppressed because it is too large Load Diff