diff --git a/apps/api/v2/package.json b/apps/api/v2/package.json index 94c876b7b9..704c089eb2 100644 --- a/apps/api/v2/package.json +++ b/apps/api/v2/package.json @@ -28,7 +28,7 @@ "dependencies": { "@calcom/platform-constants": "*", "@calcom/platform-enums": "*", - "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.39", + "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.40", "@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2", "@calcom/platform-types": "*", "@calcom/platform-utils": "*", diff --git a/apps/api/v2/src/ee/calendars/calendars.interface.ts b/apps/api/v2/src/ee/calendars/calendars.interface.ts index 9767e7ee81..03fca182dd 100644 --- a/apps/api/v2/src/ee/calendars/calendars.interface.ts +++ b/apps/api/v2/src/ee/calendars/calendars.interface.ts @@ -1,3 +1,4 @@ +import { CreateIcsFeedOutputResponseDto } from "@/ee/calendars/input/create-ics.output"; import { Request } from "express"; import { ApiResponse } from "@calcom/platform-types"; @@ -12,6 +13,16 @@ export interface CredentialSyncCalendarApp { check(userId: number): Promise; } +export interface ICSFeedCalendarApp { + save( + userId: number, + userEmail: string, + urls: string[], + readonly?: boolean + ): Promise; + check(userId: number): Promise; +} + export interface OAuthCalendarApp extends CalendarApp { connect(authorization: string, req: Request): Promise>; } diff --git a/apps/api/v2/src/ee/calendars/calendars.module.ts b/apps/api/v2/src/ee/calendars/calendars.module.ts index 65fa4253e7..09c367369f 100644 --- a/apps/api/v2/src/ee/calendars/calendars.module.ts +++ b/apps/api/v2/src/ee/calendars/calendars.module.ts @@ -3,6 +3,7 @@ import { CalendarsController } from "@/ee/calendars/controllers/calendars.contro import { AppleCalendarService } from "@/ee/calendars/services/apple-calendar.service"; import { CalendarsService } from "@/ee/calendars/services/calendars.service"; import { GoogleCalendarService } from "@/ee/calendars/services/gcal.service"; +import { IcsFeedService } from "@/ee/calendars/services/ics-feed.service"; import { OutlookService } from "@/ee/calendars/services/outlook.service"; import { AppsRepository } from "@/modules/apps/apps.repository"; import { CredentialsRepository } from "@/modules/credentials/credentials.repository"; @@ -20,6 +21,7 @@ import { Module } from "@nestjs/common"; OutlookService, GoogleCalendarService, AppleCalendarService, + IcsFeedService, SelectedCalendarsRepository, AppsRepository, CalendarsRepository, diff --git a/apps/api/v2/src/ee/calendars/controllers/calendars.controller.e2e-spec.ts b/apps/api/v2/src/ee/calendars/controllers/calendars.controller.e2e-spec.ts index 5c1ab8ff06..1ad339e1e9 100644 --- a/apps/api/v2/src/ee/calendars/controllers/calendars.controller.e2e-spec.ts +++ b/apps/api/v2/src/ee/calendars/controllers/calendars.controller.e2e-spec.ts @@ -1,5 +1,6 @@ import { bootstrap } from "@/app"; import { AppModule } from "@/app.module"; +import { CreateIcsFeedOutput, CreateIcsFeedOutputResponseDto } from "@/ee/calendars/input/create-ics.output"; import { DeletedCalendarCredentialsOutputResponseDto } from "@/ee/calendars/outputs/delete-calendar-credentials.output"; import { CalendarsService } from "@/ee/calendars/services/calendars.service"; import { HttpExceptionFilter } from "@/filters/http-exception.filter"; @@ -18,6 +19,7 @@ import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository. import { TokensRepositoryFixture } from "test/fixtures/repository/tokens.repository.fixture"; import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture"; import { CalendarsServiceMock } from "test/mocks/calendars-service-mock"; +import { IcsCalendarServiceMock } from "test/mocks/ics-calendar-service-mock"; import { SUCCESS_STATUS } from "@calcom/platform-constants"; import { @@ -27,6 +29,8 @@ import { GOOGLE_CALENDAR_ID, } from "@calcom/platform-constants"; import { OFFICE_365_CALENDAR_ID, OFFICE_365_CALENDAR_TYPE } from "@calcom/platform-constants"; +import { ICS_CALENDAR } from "@calcom/platform-constants/apps"; +import { IcsFeedCalendarService } from "@calcom/platform-libraries"; const CLIENT_REDIRECT_URI = "http://localhost:5555"; @@ -45,6 +49,7 @@ describe("Platform Calendars Endpoints", () => { let googleCalendarCredentials: Credential; let accessTokenSecret: string; let refreshTokenSecret: string; + let icsCalendarCredentials: CreateIcsFeedOutput; beforeAll(async () => { const moduleRef = await Test.createTestingModule({ @@ -192,6 +197,52 @@ describe("Platform Calendars Endpoints", () => { .expect(200); }); + it(`/POST/v2/calendars/${ICS_CALENDAR}/save with access token should fail to create a new ics feed calendar credentials with invalid urls`, async () => { + const body = { + urls: ["https://cal.com/ics/feed.ics", "https://not-an-ics-feed.com"], + readOnly: false, + }; + await request(app.getHttpServer()) + .post(`/v2/calendars/${ICS_CALENDAR}/save`) + .set("Authorization", `Bearer ${accessTokenSecret}`) + .set("Origin", CLIENT_REDIRECT_URI) + .send(body) + .expect(400); + }); + + it(`/POST/v2/calendars/${ICS_CALENDAR}/save with access token should create a new ics feed calendar credentials`, async () => { + const body = { + urls: ["https://cal.com/ics/feed.ics"], + readOnly: false, + }; + jest + .spyOn(IcsFeedCalendarService.prototype, "listCalendars") + .mockImplementation(IcsCalendarServiceMock.prototype.listCalendars); + await request(app.getHttpServer()) + .post(`/v2/calendars/${ICS_CALENDAR}/save`) + .set("Authorization", `Bearer ${accessTokenSecret}`) + .set("Origin", CLIENT_REDIRECT_URI) + .send(body) + .expect(201) + .then(async (response) => { + const responseBody: CreateIcsFeedOutputResponseDto = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + expect(responseBody.data.userId).toBeDefined(); + expect(responseBody.data.userId).toEqual(user.id); + expect(responseBody.data.id).toBeDefined(); + icsCalendarCredentials = responseBody.data; + }); + }); + + it(`/GET/v2/calendars/${ICS_CALENDAR}/check with access token`, async () => { + await request(app.getHttpServer()) + .get(`/v2/calendars/${ICS_CALENDAR}/check`) + .set("Authorization", `Bearer ${accessTokenSecret}`) + .set("Origin", CLIENT_REDIRECT_URI) + .expect(200); + }); + it.skip(`/POST/v2/calendars/${OFFICE_365_CALENDAR}/disconnect: it should respond with a 201 returning back the user deleted calendar credentials`, async () => { const body = { id: 10, @@ -217,6 +268,7 @@ describe("Platform Calendars Endpoints", () => { await teamRepositoryFixture.delete(organization.id); await credentialsRepositoryFixture.delete(office365Credentials.id); await credentialsRepositoryFixture.delete(googleCalendarCredentials.id); + await credentialsRepositoryFixture.delete(icsCalendarCredentials.id); await userRepositoryFixture.deleteByEmail(user.email); await app.close(); }); diff --git a/apps/api/v2/src/ee/calendars/controllers/calendars.controller.ts b/apps/api/v2/src/ee/calendars/controllers/calendars.controller.ts index c9a805a63a..452f011b94 100644 --- a/apps/api/v2/src/ee/calendars/controllers/calendars.controller.ts +++ b/apps/api/v2/src/ee/calendars/controllers/calendars.controller.ts @@ -1,4 +1,6 @@ import { CalendarsRepository } from "@/ee/calendars/calendars.repository"; +import { CreateIcsFeedInputDto } from "@/ee/calendars/input/create-ics.input"; +import { CreateIcsFeedOutputResponseDto } from "@/ee/calendars/input/create-ics.output"; import { DeleteCalendarCredentialsInputBodyDto } from "@/ee/calendars/input/delete-calendar-credentials.input"; import { GetBusyTimesOutput } from "@/ee/calendars/outputs/busy-times.output"; import { ConnectedCalendarsOutput } from "@/ee/calendars/outputs/connected-calendars.output"; @@ -9,6 +11,7 @@ import { import { AppleCalendarService } from "@/ee/calendars/services/apple-calendar.service"; import { CalendarsService } from "@/ee/calendars/services/calendars.service"; import { GoogleCalendarService } from "@/ee/calendars/services/gcal.service"; +import { IcsFeedService } from "@/ee/calendars/services/ics-feed.service"; import { OutlookService } from "@/ee/calendars/services/outlook.service"; import { API_VERSIONS_VALUES } from "@/lib/api-versions"; import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; @@ -44,6 +47,7 @@ import { GOOGLE_CALENDAR, OFFICE_365_CALENDAR, APPLE_CALENDAR, + CREDENTIAL_CALENDARS, } from "@calcom/platform-constants"; import { ApiResponse, CalendarBusyTimesInput } from "@calcom/platform-types"; @@ -58,9 +62,26 @@ export class CalendarsController { private readonly outlookService: OutlookService, private readonly googleCalendarService: GoogleCalendarService, private readonly appleCalendarService: AppleCalendarService, + private readonly icsFeedService: IcsFeedService, private readonly calendarsRepository: CalendarsRepository ) {} + @Post("/ics-feed/save") + @UseGuards(ApiAuthGuard) + async createIcsFeed( + @GetUser("id") userId: number, + @GetUser("email") userEmail: string, + @Body() body: CreateIcsFeedInputDto + ): Promise { + return await this.icsFeedService.save(userId, userEmail, body.urls, body.readOnly); + } + + @Get("/ics-feed/check") + @UseGuards(ApiAuthGuard) + async checkIcsFeed(@GetUser("id") userId: number): Promise { + return await this.icsFeedService.check(userId); + } + @UseGuards(ApiAuthGuard) @Get("/busy-times") @ApiOperation({ summary: "Get busy times" }) @@ -172,7 +193,7 @@ export class CalendarsController { default: throw new BadRequestException( "Invalid calendar type, available calendars are: ", - CALENDARS.join(", ") + CREDENTIAL_CALENDARS.join(", ") ); } } diff --git a/apps/api/v2/src/ee/calendars/input/create-ics.input.ts b/apps/api/v2/src/ee/calendars/input/create-ics.input.ts new file mode 100644 index 0000000000..17e9893c03 --- /dev/null +++ b/apps/api/v2/src/ee/calendars/input/create-ics.input.ts @@ -0,0 +1,62 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { + ArrayNotEmpty, + IsBoolean, + IsOptional, + Validate, + ValidatorConstraint, + ValidatorConstraintInterface, +} from "class-validator"; +import { IsNotEmpty, IsArray } from "class-validator"; + +// Custom constraint to validate ICS URLs +@ValidatorConstraint({ async: false }) +export class IsICSUrlConstraint implements ValidatorConstraintInterface { + validate(url: unknown) { + if (typeof url !== "string") return false; + + // Check if it's a valid URL and ends with .ics + try { + const urlObject = new URL(url); + return ( + (urlObject.protocol === "http:" || urlObject.protocol === "https:") && + urlObject.pathname.endsWith(".ics") + ); + } catch (error) { + return false; + } + } + + defaultMessage() { + return "The URL must be a valid ICS URL (ending with .ics)"; + } +} + +export class CreateIcsFeedInputDto { + @ApiProperty({ + example: ["https://cal.com/ics/feed.ics", "http://cal.com/ics/feed.ics"], + description: "An array of ICS URLs", + type: "array", + items: { + type: "string", + example: "https://cal.com/ics/feed.ics", + }, + required: true, + }) + @IsArray() + @ArrayNotEmpty() + @IsNotEmpty({ each: true }) + @Validate(IsICSUrlConstraint, { each: true }) // Apply the custom validator to each element in the array + urls!: string[]; + + @IsBoolean() + @ApiProperty({ + example: false, + description: "Whether to allowing writing to the calendar or not", + type: "boolean", + required: false, + default: true, + }) + @IsOptional() + readOnly?: boolean = true; +} diff --git a/apps/api/v2/src/ee/calendars/input/create-ics.output.ts b/apps/api/v2/src/ee/calendars/input/create-ics.output.ts new file mode 100644 index 0000000000..6f9de1c300 --- /dev/null +++ b/apps/api/v2/src/ee/calendars/input/create-ics.output.ts @@ -0,0 +1,57 @@ +import { ApiProperty } from "@nestjs/swagger"; +import { Expose, Type } from "class-transformer"; +import { IsString, ValidateNested, IsEnum, IsInt, IsBoolean } from "class-validator"; + +import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants"; + +export class CreateIcsFeedOutput { + @IsInt() + @Expose() + @ApiProperty({ example: 1234567890, description: "The id of the calendar credential" }) + readonly id!: number; + + @IsString() + @Expose() + @ApiProperty({ example: "ics-feed_calendar", description: "The type of the calendar" }) + readonly type!: string; + + @IsInt() + @Expose() + @ApiProperty({ + example: 1234567890, + description: "The user id of the user that created the calendar", + type: "integer", + }) + readonly userId!: number | null; + + @IsInt() + @Expose() + @ApiProperty({ + example: 1234567890, + nullable: true, + description: "The team id of the user that created the calendar", + type: "integer", + }) + readonly teamId!: number | null; + + @IsString() + @Expose() + @ApiProperty({ example: "ics-feed", description: "The slug of the calendar" }) + readonly appId!: string | null; + + @IsBoolean() + @Expose() + @ApiProperty({ example: false, description: "Whether the calendar credentials are valid or not" }) + readonly invalid!: boolean | null; +} + +export class CreateIcsFeedOutputResponseDto { + @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) + @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) + status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; + + @Expose() + @ValidateNested() + @Type(() => CreateIcsFeedOutput) + data!: CreateIcsFeedOutput; +} diff --git a/apps/api/v2/src/ee/calendars/services/ics-feed.service.ts b/apps/api/v2/src/ee/calendars/services/ics-feed.service.ts new file mode 100644 index 0000000000..c18da622bc --- /dev/null +++ b/apps/api/v2/src/ee/calendars/services/ics-feed.service.ts @@ -0,0 +1,103 @@ +import { ICSFeedCalendarApp } from "@/ee/calendars/calendars.interface"; +import { CreateIcsFeedOutputResponseDto } from "@/ee/calendars/input/create-ics.output"; +import { CalendarsService } from "@/ee/calendars/services/calendars.service"; +import { CredentialsRepository } from "@/modules/credentials/credentials.repository"; +import { BadRequestException, UnauthorizedException, Logger } from "@nestjs/common"; +import { Injectable } from "@nestjs/common"; + +import { SUCCESS_STATUS, ICS_CALENDAR_TYPE, ICS_CALENDAR } from "@calcom/platform-constants"; +import { symmetricEncrypt, IcsFeedCalendarService } from "@calcom/platform-libraries"; + +@Injectable() +export class IcsFeedService implements ICSFeedCalendarApp { + constructor( + private readonly calendarsService: CalendarsService, + private readonly credentialRepository: CredentialsRepository + ) {} + + private logger = new Logger("IcsFeedService"); + + async save( + userId: number, + userEmail: string, + urls: string[], + readonly = true + ): Promise { + const data = { + type: ICS_CALENDAR_TYPE, + ICS_CALENDAR, + key: symmetricEncrypt( + JSON.stringify({ urls, skipWriting: readonly }), + process.env.CALENDSO_ENCRYPTION_KEY || "" + ), + userId: userId, + teamId: null, + appId: ICS_CALENDAR, + invalid: false, + }; + + try { + const dav = new IcsFeedCalendarService({ + id: 0, + ...data, + user: { email: userEmail }, + }); + + const listedCals = await dav.listCalendars(); + + if (listedCals.length !== urls.length) { + throw new BadRequestException( + `Listed cals and URLs mismatch: ${listedCals.length} vs. ${urls.length}` + ); + } + + const credential = await this.credentialRepository.upsertAppCredential( + ICS_CALENDAR_TYPE, + data.key, + userId + ); + return { + status: SUCCESS_STATUS, + data: { + id: credential.id, + type: credential.type, + userId: credential.userId, + teamId: credential.teamId, + appId: credential.appId, + invalid: credential.invalid, + }, + }; + } catch (e) { + this.logger.error("Could not add ICS feeds", e); + throw new BadRequestException("Could not add ICS feeds, try using private ics feed."); + } + } + + async check(userId: number): Promise<{ status: typeof SUCCESS_STATUS }> { + const icsFeedCredentials = await this.credentialRepository.getByTypeAndUserId(ICS_CALENDAR_TYPE, userId); + + if (!icsFeedCredentials) { + throw new BadRequestException("Credentials for Ics Feed calendar not found."); + } + + if (icsFeedCredentials.invalid) { + throw new BadRequestException("Invalid Ics Feed credentials."); + } + + const { connectedCalendars } = await this.calendarsService.getCalendars(userId); + const icsCalendar = connectedCalendars.find( + (cal: { integration: { type: string } }) => cal.integration.type === ICS_CALENDAR_TYPE + ); + + if (!icsCalendar) { + throw new UnauthorizedException("Ics Feed not connected."); + } + if (icsCalendar.error?.message) { + throw new UnauthorizedException(icsCalendar.error?.message); + } + + return { + status: SUCCESS_STATUS, + }; + } +} diff --git a/apps/api/v2/swagger/documentation.json b/apps/api/v2/swagger/documentation.json index 86d6c79fce..84980f6385 100644 --- a/apps/api/v2/swagger/documentation.json +++ b/apps/api/v2/swagger/documentation.json @@ -3622,6 +3622,58 @@ ] } }, + "/v2/calendars/ics-feed/save": { + "post": { + "operationId": "CalendarsController_createIcsFeed", + "parameters": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateIcsFeedInputDto" + } + } + } + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateIcsFeedOutputResponseDto" + } + } + } + } + }, + "tags": [ + "Calendars" + ] + } + }, + "/v2/calendars/ics-feed/check": { + "get": { + "operationId": "CalendarsController_checkIcsFeed", + "parameters": [], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "object" + } + } + } + } + }, + "tags": [ + "Calendars" + ] + } + }, "/v2/calendars/busy-times": { "get": { "operationId": "CalendarsController_getBusyTimes", @@ -11017,6 +11069,99 @@ "data" ] }, + "CreateIcsFeedInputDto": { + "type": "object", + "properties": { + "urls": { + "type": "array", + "example": [ + "https://cal.com/ics/feed.ics", + "http://cal.com/ics/feed.ics" + ], + "description": "An array of ICS URLs", + "items": { + "type": "string", + "example": "https://cal.com/ics/feed.ics" + } + }, + "readOnly": { + "type": "boolean", + "default": true, + "example": false, + "description": "Whether to allowing writing to the calendar or not" + } + }, + "required": [ + "urls" + ] + }, + "CreateIcsFeedOutput": { + "type": "object", + "properties": { + "id": { + "type": "number", + "example": 1234567890, + "description": "The id of the calendar credential" + }, + "type": { + "type": "string", + "example": "ics-feed_calendar", + "description": "The type of the calendar" + }, + "userId": { + "type": "integer", + "nullable": true, + "example": 1234567890, + "description": "The user id of the user that created the calendar" + }, + "teamId": { + "type": "integer", + "nullable": true, + "example": 1234567890, + "description": "The team id of the user that created the calendar" + }, + "appId": { + "type": "string", + "nullable": true, + "example": "ics-feed", + "description": "The slug of the calendar" + }, + "invalid": { + "type": "boolean", + "nullable": true, + "example": false, + "description": "Whether the calendar credentials are valid or not" + } + }, + "required": [ + "id", + "type", + "userId", + "teamId", + "appId", + "invalid" + ] + }, + "CreateIcsFeedOutputResponseDto": { + "type": "object", + "properties": { + "status": { + "type": "string", + "example": "success", + "enum": [ + "success", + "error" + ] + }, + "data": { + "$ref": "#/components/schemas/CreateIcsFeedOutput" + } + }, + "required": [ + "status", + "data" + ] + }, "BusyTimesOutput": { "type": "object", "properties": { diff --git a/apps/api/v2/test/mocks/calendars-service-mock.ts b/apps/api/v2/test/mocks/calendars-service-mock.ts index 9730237aa5..85dd5a0f81 100644 --- a/apps/api/v2/test/mocks/calendars-service-mock.ts +++ b/apps/api/v2/test/mocks/calendars-service-mock.ts @@ -1,5 +1,7 @@ import { CalendarsService } from "@/ee/calendars/services/calendars.service"; +import { ICS_CALENDAR_ID, ICS_CALENDAR_TYPE } from "@calcom/platform-constants"; + export class CalendarsServiceMock { async getCalendars() { return { @@ -42,6 +44,25 @@ export class CalendarsServiceMock { credentialId: 2, error: { message: "" }, }, + { + integration: { + installed: false, + type: ICS_CALENDAR_TYPE, + title: "ics-feed_calendar", + name: "ics-feed_calendar", + description: "", + variant: "calendar", + slug: ICS_CALENDAR_ID, + locationOption: null, + categories: ["calendar"], + logo: "", + publisher: "", + url: "", + email: "", + }, + credentialId: 2, + error: { message: "" }, + }, ], destinationCalendar: { name: "destinationCalendar", diff --git a/apps/api/v2/test/mocks/ics-calendar-service-mock.ts b/apps/api/v2/test/mocks/ics-calendar-service-mock.ts new file mode 100644 index 0000000000..6c9ddcfdd1 --- /dev/null +++ b/apps/api/v2/test/mocks/ics-calendar-service-mock.ts @@ -0,0 +1,27 @@ +export class IcsCalendarServiceMock { + async listCalendars() { + return [ + { + name: "name", + readOnly: true, + externalId: "externalId", + integrationName: "ics-feed_calendar", + primary: true, + email: "email", + primaryEmail: "primaryEmail", + credentialId: 1, + integrationTitle: "integrationTitle", + }, + ] satisfies { + primary?: boolean; + name?: string; + readOnly?: boolean; + email?: string; + primaryEmail?: string; + credentialId?: number | null; + integrationTitle?: string; + externalId?: string; + integrationName?: string; + }[]; + } +} diff --git a/apps/api/v2/test/setEnvVars.ts b/apps/api/v2/test/setEnvVars.ts index 102a16fc13..1f50272d84 100644 --- a/apps/api/v2/test/setEnvVars.ts +++ b/apps/api/v2/test/setEnvVars.ts @@ -22,7 +22,9 @@ const env: Partial> = { process.env = { ...env, ...process.env, + // fake keys for testing NEXT_PUBLIC_VAPID_PUBLIC_KEY: "BIds0AQJ96xGBjTSMHTOqLBLutQE7Lu32KKdgSdy7A2cS4mKI2cgb3iGkhDJa5Siy-stezyuPm8qpbhmNxdNHMw", VAPID_PRIVATE_KEY: "6cJtkASCar5sZWguIAW7OjvyixpBw9p8zL8WDDwk9Jk", + CALENDSO_ENCRYPTION_KEY: "22gfxhWUlcKliUeXcu8xNah2+HP/29ZX", }; diff --git a/packages/platform/constants/apps.ts b/packages/platform/constants/apps.ts index 8872c142c9..8469988d89 100644 --- a/packages/platform/constants/apps.ts +++ b/packages/platform/constants/apps.ts @@ -5,12 +5,17 @@ export const OFFICE_365_CALENDAR_ID = "office365-calendar"; export const GOOGLE_CALENDAR = "google"; export const OFFICE_365_CALENDAR = "office365"; export const APPLE_CALENDAR = "apple"; +export const ICS_CALENDAR = "ics-feed"; +export const ICS_CALENDAR_ID = "ics-feed"; export const APPLE_CALENDAR_TYPE = "apple_calendar"; +export const ICS_CALENDAR_TYPE = "ics-feed_calendar"; export const APPLE_CALENDAR_ID = "apple-calendar"; export const CALENDARS = [GOOGLE_CALENDAR, OFFICE_365_CALENDAR, APPLE_CALENDAR] as const; +export const CREDENTIAL_CALENDARS = [APPLE_CALENDAR] as const; export const APPS_TYPE_ID_MAPPING = { [GOOGLE_CALENDAR_TYPE]: GOOGLE_CALENDAR_ID, [OFFICE_365_CALENDAR_TYPE]: OFFICE_365_CALENDAR_ID, [APPLE_CALENDAR_TYPE]: APPLE_CALENDAR_ID, + [ICS_CALENDAR_TYPE]: ICS_CALENDAR_ID, } as const; diff --git a/packages/platform/libraries/index.ts b/packages/platform/libraries/index.ts index 751798d986..72a8ed4db9 100644 --- a/packages/platform/libraries/index.ts +++ b/packages/platform/libraries/index.ts @@ -1,5 +1,6 @@ import { getCalendar } from "@calcom/app-store/_utils/getCalendar"; import { CalendarService } from "@calcom/app-store/applecalendar/lib"; +import { CalendarService as IcsFeedCalendarService } from "@calcom/app-store/ics-feedcalendar/lib"; import { getBookingForReschedule } from "@calcom/features/bookings/lib/get-booking"; import getBookingInfo from "@calcom/features/bookings/lib/getBookingInfo"; import handleCancelBooking from "@calcom/features/bookings/lib/handleCancelBooking"; @@ -141,4 +142,6 @@ export { getTranslation }; export { updateNewTeamMemberEventTypes } from "@calcom/lib/server/queries"; export { ErrorCode } from "@calcom/lib/errorCodes"; + +export { IcsFeedCalendarService }; export { validateCustomEventName } from "@calcom/core/event";