Files
cal.diy/apps/api/v2/src/ee/calendars/controllers/calendars.controller.ts
T
Keith WilliamsandDevin AI 739ef153f6 docs: improve English naturalness in API v2 documentation (#22130)
* docs: improve English naturalness in API v2 documentation

- Fix awkward API summary 'Find out when is an event type ready to be booked' to 'Get available time slots for an event type'
- Correct 'setup' vs 'set up' usage throughout documentation
- Fix OAuth capitalization consistency
- Improve 'api' to 'API' capitalization in v2 docs
- Enhance readability while preserving technical accuracy

Fixes unnatural English phrasing in titles, headers, descriptions, and summaries across API v2 documentation files.

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* docs: add missing articles 'the' in API v2 documentation

- Fix 'How to Set Up API' to 'How to Set Up the API' in titles
- Add 'the' before 'API' in descriptions and summaries
- Fix grammar error 'all you need to is' to 'all you need to do is'
- Improve naturalness while maintaining technical accuracy

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* docs: fix unnatural English in API v2 @ApiOperation summaries

- Fix grammatically incorrect 'conferencing apps oauths callback' to 'Conferencing app OAuth callback'
- Replace 'ooo' with 'out-of-office' in user-facing summaries for clarity
- Ensure consistent sentence case capitalization (only first letter capitalized)
- Remove trailing periods from summaries for consistency
- Fix awkward phrasing like 'Get by attribute id all of...' to more natural English
- Revert 'Introduction to the API v2' back to 'Introduction to API v2'
- Improve naturalness while maintaining technical accuracy

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* Update apps/api/v2/src/modules/slots/slots-2024-09-04/controllers/slots.controller.ts

* Update docs/api-reference/v2/openapi.json

* docs: fix setup/set up usage - use 'backend setup' (noun) not 'backend set up' (verb)

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* docs: fix setup/set up verb usage - use 'set up' (verb) not 'setup' (verb)

- Fix 'You have to setup' → 'You have to set up' in quickstart.mdx
- Fix 'Setup environment variables' → 'Set up environment variables'
- Fix 'Setup root of your app' → 'Set up root of your app'
- Fix 'prompted to setup' → 'prompted to set up' in setup.mdx

Co-Authored-By: keith@cal.com <keithwillcode@gmail.com>

* Ran v2 locally to regen the doc json files

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-07-03 23:47:09 +00:00

320 lines
11 KiB
TypeScript

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";
import {
DeletedCalendarCredentialsOutputResponseDto,
DeletedCalendarCredentialsOutputDto,
} from "@/ee/calendars/outputs/delete-calendar-credentials.output";
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 { API_KEY_OR_ACCESS_TOKEN_HEADER } from "@/lib/docs/headers";
import { ApiAuthGuardOnlyAllow } from "@/modules/auth/decorators/api-auth-guard-only-allow.decorator";
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
import { UserWithProfile } from "@/modules/users/users.repository";
import {
Controller,
Get,
UseGuards,
Query,
HttpStatus,
HttpCode,
Req,
Param,
Headers,
Redirect,
BadRequestException,
Post,
Body,
ParseBoolPipe,
} from "@nestjs/common";
import { ApiHeader, ApiOperation, ApiParam, ApiQuery, ApiTags as DocsTags } from "@nestjs/swagger";
import { User } from "@prisma/client";
import { plainToClass } from "class-transformer";
import { Request } from "express";
import { z } from "zod";
import { APPS_READ } from "@calcom/platform-constants";
import {
SUCCESS_STATUS,
CALENDARS,
GOOGLE_CALENDAR,
OFFICE_365_CALENDAR,
APPLE_CALENDAR,
CREDENTIAL_CALENDARS,
} from "@calcom/platform-constants";
import { ApiResponse, CalendarBusyTimesInput, CreateCalendarCredentialsInput } from "@calcom/platform-types";
export interface CalendarState {
accessToken: string;
origin: string;
redir?: string;
isDryRun?: boolean;
}
const calendarStateSchema = z.object({
accessToken: z.string(),
origin: z.string(),
redir: z.string().optional(),
isDryRun: z
.string()
.optional()
.transform((val) => val === "true"),
});
@Controller({
path: "/v2/calendars",
version: API_VERSIONS_VALUES,
})
@DocsTags("Calendars")
export class CalendarsController {
constructor(
private readonly calendarsService: CalendarsService,
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)
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@ApiOperation({ summary: "Save an ICS feed" })
async createIcsFeed(
@GetUser("id") userId: number,
@GetUser("email") userEmail: string,
@Body() body: CreateIcsFeedInputDto
): Promise<CreateIcsFeedOutputResponseDto> {
return await this.icsFeedService.save(userId, userEmail, body.urls, body.readOnly);
}
@Get("/ics-feed/check")
@UseGuards(ApiAuthGuard)
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@ApiOperation({ summary: "Check an ICS feed" })
async checkIcsFeed(@GetUser("id") userId: number): Promise<ApiResponse> {
return await this.icsFeedService.check(userId);
}
@UseGuards(ApiAuthGuard)
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@Get("/busy-times")
@ApiOperation({
summary: "Get busy times",
description:
"Get busy times from a calendar. Example request URL is `https://api.cal.com/v2/calendars/busy-times?loggedInUsersTz=Europe%2FMadrid&dateFrom=2024-12-18&dateTo=2024-12-18&calendarsToLoad[0][credentialId]=135&calendarsToLoad[0][externalId]=skrauciz%40gmail.com`",
})
async getBusyTimes(
@Query() queryParams: CalendarBusyTimesInput,
@GetUser() user: UserWithProfile
): Promise<GetBusyTimesOutput> {
const { loggedInUsersTz, dateFrom, dateTo, calendarsToLoad } = queryParams;
const busyTimes = await this.calendarsService.getBusyTimes(
calendarsToLoad,
user.id,
dateFrom,
dateTo,
loggedInUsersTz
);
return {
status: SUCCESS_STATUS,
data: busyTimes,
};
}
@Get("/")
@UseGuards(ApiAuthGuard)
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@ApiOperation({ summary: "Get all calendars" })
async getCalendars(@GetUser("id") userId: number): Promise<ConnectedCalendarsOutput> {
const calendars = await this.calendarsService.getCalendars(userId);
return {
status: SUCCESS_STATUS,
data: calendars,
};
}
@ApiParam({
enum: [OFFICE_365_CALENDAR, GOOGLE_CALENDAR],
type: String,
name: "calendar",
})
@UseGuards(ApiAuthGuard)
@ApiAuthGuardOnlyAllow(["API_KEY", "ACCESS_TOKEN"])
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@Get("/:calendar/connect")
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: "Get OAuth connect URL" })
@ApiQuery({
name: "redir",
required: false,
type: String,
description: "Redirect URL after successful calendar authorization.",
})
async redirect(
@Req() req: Request,
@Headers("Authorization") authorization: string,
@Param("calendar") calendar: string,
@Query("redir") redir?: string | null,
@Query("isDryRun", new ParseBoolPipe({ optional: true })) isDryRun?: boolean
): Promise<ApiResponse<{ authUrl: string }>> {
switch (calendar) {
case OFFICE_365_CALENDAR:
return await this.outlookService.connect(authorization, req, redir ?? "", isDryRun);
case GOOGLE_CALENDAR:
return await this.googleCalendarService.connect(authorization, req, redir ?? "", isDryRun);
default:
throw new BadRequestException(
"Invalid calendar type, available calendars are: ",
CALENDARS.join(", ")
);
}
}
@ApiParam({
enum: [OFFICE_365_CALENDAR, GOOGLE_CALENDAR],
type: String,
name: "calendar",
})
@Get("/:calendar/save")
@HttpCode(HttpStatus.OK)
@Redirect(undefined, 301)
@ApiOperation({ summary: "Save Google or Outlook calendar credentials" })
async save(
@Query("state") state: string,
@Query("code") code: string,
@Param("calendar") calendar: string
): Promise<{ url: string }> {
let stateObj: CalendarState;
try {
// First try to parse as JSON
stateObj = JSON.parse(state) as CalendarState;
} catch (e) {
// If JSON parsing fails, try URL params
const stateParams = new URLSearchParams(state);
const parsedState = calendarStateSchema.parse({
accessToken: stateParams.get("accessToken"),
origin: stateParams.get("origin"),
redir: stateParams.get("redir") || undefined,
isDryRun: stateParams.get("isDryRun"),
});
stateObj = parsedState;
}
const { accessToken, origin, redir, isDryRun } = stateObj;
switch (calendar) {
case OFFICE_365_CALENDAR:
return await this.outlookService.save(code, accessToken, origin, redir ?? "", !!isDryRun);
case GOOGLE_CALENDAR:
return await this.googleCalendarService.save(code, accessToken, origin, redir ?? "", !!isDryRun);
default:
throw new BadRequestException(
"Invalid calendar type, available calendars are: ",
CALENDARS.join(", ")
);
}
}
@ApiParam({
enum: [APPLE_CALENDAR],
type: String,
name: "calendar",
})
@UseGuards(ApiAuthGuard)
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@Post("/:calendar/credentials")
@ApiOperation({ summary: "Save Apple calendar credentials" })
async syncCredentials(
@GetUser() user: User,
@Param("calendar") calendar: string,
@Body() body: CreateCalendarCredentialsInput
): Promise<{ status: string }> {
const { username, password } = body;
switch (calendar) {
case APPLE_CALENDAR:
return await this.appleCalendarService.save(user.id, user.email, username, password);
default:
throw new BadRequestException(
"Invalid calendar type, available calendars are: ",
CREDENTIAL_CALENDARS.join(", ")
);
}
}
@ApiParam({
enum: [APPLE_CALENDAR, GOOGLE_CALENDAR, OFFICE_365_CALENDAR],
type: String,
name: "calendar",
})
@Get("/:calendar/check")
@HttpCode(HttpStatus.OK)
@UseGuards(ApiAuthGuard, PermissionsGuard)
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@Permissions([APPS_READ])
@ApiOperation({ summary: "Check a calendar connection" })
async check(@GetUser("id") userId: number, @Param("calendar") calendar: string): Promise<ApiResponse> {
switch (calendar) {
case OFFICE_365_CALENDAR:
return await this.outlookService.check(userId);
case GOOGLE_CALENDAR:
return await this.googleCalendarService.check(userId);
case APPLE_CALENDAR:
return await this.appleCalendarService.check(userId);
default:
throw new BadRequestException(
"Invalid calendar type, available calendars are: ",
CALENDARS.join(", ")
);
}
}
@ApiParam({
enum: [APPLE_CALENDAR, GOOGLE_CALENDAR, OFFICE_365_CALENDAR],
type: String,
name: "calendar",
})
@UseGuards(ApiAuthGuard)
@ApiHeader(API_KEY_OR_ACCESS_TOKEN_HEADER)
@Post("/:calendar/disconnect")
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: "Disconnect a calendar" })
async deleteCalendarCredentials(
@Param("calendar") calendar: string,
@Body() body: DeleteCalendarCredentialsInputBodyDto,
@GetUser() user: UserWithProfile
): Promise<DeletedCalendarCredentialsOutputResponseDto> {
const { id: credentialId } = body;
await this.calendarsService.checkCalendarCredentials(credentialId, user.id);
const { id, type, userId, teamId, appId, invalid } = await this.calendarsRepository.deleteCredentials(
credentialId
);
return {
status: SUCCESS_STATUS,
data: plainToClass(
DeletedCalendarCredentialsOutputDto,
{ id, type, userId, teamId, appId, invalid },
{ strategy: "excludeAll" }
),
};
}
}