import type { drive_v3 } from "@googleapis/drive" import { Readable } from "node:stream" import * as z from "zod" import { defineAction } from "../../automation/actions" import { GOOGLE_DRIVE_ADMIN_REQUIREMENT, GOOGLE_DRIVE_COMMENT_SCHEMA, GOOGLE_DRIVE_FILE_SCHEMA, GOOGLE_DRIVE_METADATA_READ_REQUIREMENT, GOOGLE_DRIVE_METADATA_WRITE_REQUIREMENT, GOOGLE_DRIVE_PERMISSION_SCHEMA, GOOGLE_DRIVE_READ_REQUIREMENT, GOOGLE_SHARED_DRIVE_READ_REQUIREMENT, GOOGLE_DRIVE_REPLY_SCHEMA, GOOGLE_DRIVE_WRITE_REQUIREMENT, GOOGLE_SHARED_DRIVE_SCHEMA, getGoogleDriveApi, googleDriveCommentFields, googleDriveFileFields, googleDrivePermissionFields, googleDriveReplyFields, googleSharedDriveFields, normalizeGoogleDriveComment, normalizeGoogleDriveFile, normalizeGoogleDrivePermission, normalizeGoogleDriveReply, normalizeGoogleSharedDrive, } from "./lib" const FILE_ID_INPUT = z.object({ fileId: z.string().min(1) }) const COMMENT_ID_INPUT = FILE_ID_INPUT.extend({ commentId: z.string().min(1) }) const REPLY_ID_INPUT = COMMENT_ID_INPUT.extend({ replyId: z.string().min(1) }) const PERMISSION_ID_INPUT = FILE_ID_INPUT.extend({ permissionId: z.string().min(1), }) const SHARED_DRIVE_ID_INPUT = z.object({ driveId: z.string().min(1) }) const PAGE_INPUT = { pageSize: z.number().int().min(1).max(1_000).optional(), pageToken: z.string().min(1).optional(), } const PAGE_100_INPUT = { ...PAGE_INPUT, pageSize: z.number().int().min(1).max(100).optional(), } const PERMISSION_EXPIRATION_SCHEMA = z.iso .datetime({ offset: true }) .refine( isValidPermissionExpiration, "Expiration must be in the future and no more than one year away.", ) export const getGoogleDriveAbout = defineAction("Get Google Drive account") .describe("Gets connected Drive storage and user metadata.") .account("google", GOOGLE_DRIVE_METADATA_READ_REQUIREMENT) .input(z.object({})) .output( z.object({ storageLimit: z.string().optional(), storageUsage: z.string().optional(), user: z.object({ displayName: z.string().optional(), emailAddress: z.email().optional(), permissionId: z.string().optional(), }), }), ) .retry({ replaySafety: "safe" }) .handler(async ({ account }) => { const { data } = await getGoogleDriveApi(account.secret).about.get({ fields: "storageQuota(limit,usage),user(displayName,emailAddress,permissionId)", }) return { storageLimit: data.storageQuota?.limit ?? undefined, storageUsage: data.storageQuota?.usage ?? undefined, user: { displayName: data.user?.displayName ?? undefined, emailAddress: data.user?.emailAddress ?? undefined, permissionId: data.user?.permissionId ?? undefined, }, } }) export const createGoogleDriveFolder = defineAction( "Create Google Drive folder", ) .describe("Creates a folder in My Drive or a shared drive.") .account("google", GOOGLE_DRIVE_WRITE_REQUIREMENT) .input( z.object({ description: z.string().optional(), name: z.string().trim().min(1), parentId: z.string().min(1).optional(), }), ) .output(GOOGLE_DRIVE_FILE_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const { data } = await getGoogleDriveApi(account.secret).files.create({ fields: googleDriveFileFields(), requestBody: { description: input.description, mimeType: "application/vnd.google-apps.folder", name: input.name, parents: input.parentId ? [input.parentId] : undefined, }, supportsAllDrives: true, }) return normalizeGoogleDriveFile(data) }) export const uploadGoogleDriveFile = defineAction("Upload Google Drive file") .describe("Uploads file content and returns its Drive metadata.") .account("google", GOOGLE_DRIVE_WRITE_REQUIREMENT) .input( z.object({ description: z.string().optional(), file: z.instanceof(File), mimeType: z.string().min(1).optional(), name: z.string().trim().min(1).optional(), parentId: z.string().min(1).optional(), }), ) .output(GOOGLE_DRIVE_FILE_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const { data } = await getGoogleDriveApi(account.secret).files.create({ fields: googleDriveFileFields(), media: { body: Readable.from(Buffer.from(await input.file.arrayBuffer())), mimeType: (input.mimeType ?? input.file.type) || "application/octet-stream", }, requestBody: { description: input.description, name: input.name ?? input.file.name, parents: input.parentId ? [input.parentId] : undefined, }, supportsAllDrives: true, }) return normalizeGoogleDriveFile(data) }) export const getGoogleDriveFile = defineAction("Get Google Drive file") .describe("Gets metadata for a Drive file or folder.") .account("google", GOOGLE_DRIVE_METADATA_READ_REQUIREMENT) .input(FILE_ID_INPUT) .output(GOOGLE_DRIVE_FILE_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const { data } = await getGoogleDriveApi(account.secret).files.get({ fileId: input.fileId, fields: googleDriveFileFields(), supportsAllDrives: true, }) return normalizeGoogleDriveFile(data) }) export const listGoogleDriveFiles = defineAction("List Google Drive files") .describe("Lists or searches Drive files with native Drive query syntax.") .account("google", GOOGLE_DRIVE_METADATA_READ_REQUIREMENT) .input( z .object({ corpus: z.enum(["user", "drive", "domain", "allDrives"]).optional(), driveId: z.string().min(1).optional(), includeTrashed: z.boolean().optional(), orderBy: z.string().min(1).optional(), ...PAGE_INPUT, query: z.string().min(1).optional(), }) .refine( ({ corpus, driveId }) => corpus === undefined || (corpus === "drive" ? driveId !== undefined : driveId === undefined), "Provide driveId exactly when corpus is drive.", ), ) .output( z.object({ files: GOOGLE_DRIVE_FILE_SCHEMA.array(), incompleteSearch: z.boolean(), nextPageToken: z.string().optional(), }), ) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { // Keep the effective provider query visible as one semantic value. const query = [ input.query ? `(${input.query})` : undefined, input.includeTrashed ? undefined : "trashed = false", ] .filter(Boolean) .join(" and ") const { data } = await getGoogleDriveApi(account.secret).files.list({ corpora: input.corpus ?? (input.driveId ? "drive" : undefined), driveId: input.driveId, fields: `incompleteSearch,nextPageToken,files(${googleDriveFileFields()})`, includeItemsFromAllDrives: true, orderBy: input.orderBy, pageSize: input.pageSize, pageToken: input.pageToken, q: query || undefined, supportsAllDrives: true, }) return { files: (data.files ?? []).map(normalizeGoogleDriveFile), incompleteSearch: data.incompleteSearch ?? false, nextPageToken: data.nextPageToken ?? undefined, } }) export const copyGoogleDriveFile = defineAction("Copy Google Drive file") .describe("Copies a Drive file with optional new metadata.") .account("google", GOOGLE_DRIVE_WRITE_REQUIREMENT) .input( FILE_ID_INPUT.extend({ description: z.string().optional(), name: z.string().trim().min(1).optional(), parentId: z.string().min(1).optional(), }), ) .output(GOOGLE_DRIVE_FILE_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const { data } = await getGoogleDriveApi(account.secret).files.copy({ fileId: input.fileId, fields: googleDriveFileFields(), requestBody: { description: input.description, name: input.name, parents: input.parentId ? [input.parentId] : undefined, }, supportsAllDrives: true, }) return normalizeGoogleDriveFile(data) }) const UPDATE_FILE_INPUT = FILE_ID_INPUT.extend({ description: z.string().optional(), name: z.string().trim().min(1).optional(), starred: z.boolean().optional(), }) export const updateGoogleDriveFile = defineAction("Update Google Drive file") .describe("Updates a file's name, description, or starred state.") .account("google", GOOGLE_DRIVE_METADATA_WRITE_REQUIREMENT) .input( UPDATE_FILE_INPUT.refine( hasFileUpdate, "Provide a file property to update.", ), ) .output(GOOGLE_DRIVE_FILE_SCHEMA) .retry({ replaySafety: "safe" }) .handler( async ({ account, input }) => await updateFile(account.secret, input.fileId, { description: input.description, name: input.name, starred: input.starred, }), ) export const moveGoogleDriveFile = defineAction("Move Google Drive file") .describe("Moves a file between Drive folders.") .account("google", GOOGLE_DRIVE_METADATA_WRITE_REQUIREMENT) .input( FILE_ID_INPUT.extend({ addParentIds: z.string().min(1).array().optional(), removeParentIds: z.string().min(1).array().optional(), }).refine( ({ addParentIds, removeParentIds }) => addParentIds?.length || removeParentIds?.length, "Provide a parent to add or remove.", ), ) .output(GOOGLE_DRIVE_FILE_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const { data } = await getGoogleDriveApi(account.secret).files.update({ addParents: input.addParentIds?.join(","), fileId: input.fileId, fields: googleDriveFileFields(), removeParents: input.removeParentIds?.join(","), supportsAllDrives: true, }) return normalizeGoogleDriveFile(data) }) export const trashGoogleDriveFile = fileTrashAction( "Trash Google Drive file", true, ) export const restoreGoogleDriveFile = fileTrashAction( "Restore Google Drive file", false, ) export const deleteGoogleDriveFile = defineAction("Delete Google Drive file") .describe("Permanently deletes a Drive file owned by the connected account.") .account("google", GOOGLE_DRIVE_WRITE_REQUIREMENT) .input(FILE_ID_INPUT) .output(z.object({ fileId: z.string() })) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { await getGoogleDriveApi(account.secret).files.delete({ fileId: input.fileId, supportsAllDrives: true, }) return { fileId: input.fileId } }) export const downloadGoogleDriveFile = fileContentAction( "Download Google Drive file", "Downloads the binary content of a Drive file.", FILE_ID_INPUT.extend({ mimeType: z.string().min(1).optional(), name: z.string().trim().min(1).optional(), }), async (api, input) => await api.files.get( { alt: "media", fileId: input.fileId, supportsAllDrives: true }, { responseType: "arraybuffer" }, ), ) export const exportGoogleDriveFile = fileContentAction( "Export Google Workspace file", "Exports a Google Workspace file to another MIME type.", FILE_ID_INPUT.extend({ mimeType: z.string().min(1), name: z.string().trim().min(1).optional(), }), async (api, input) => await api.files.export( { fileId: input.fileId, mimeType: input.mimeType }, { responseType: "arraybuffer" }, ), ) export const createGoogleDrivePermission = defineAction( "Create Google Drive permission", ) .describe("Shares a file with a user, group, domain, or anyone.") .account("google", GOOGLE_DRIVE_WRITE_REQUIREMENT) .input( FILE_ID_INPUT.extend({ domain: z.string().min(1).optional(), emailAddress: z.email().optional(), emailMessage: z.string().optional(), expirationTime: PERMISSION_EXPIRATION_SCHEMA.optional(), role: z.enum([ "organizer", "fileOrganizer", "writer", "commenter", "reader", ]), sendNotificationEmail: z.boolean().optional(), type: z.enum(["user", "group", "domain", "anyone"]), }).superRefine((input, context) => { if ( (input.type === "user" || input.type === "group") && !input.emailAddress ) { context.addIssue({ code: "custom", message: `An email address is required for ${input.type} permissions.`, path: ["emailAddress"], }) } if (input.type === "domain" && !input.domain) { context.addIssue({ code: "custom", message: "A domain is required for domain permissions.", path: ["domain"], }) } if ( input.expirationTime && input.type !== "user" && input.type !== "group" ) { context.addIssue({ code: "custom", message: "Expiration is available only for user and group permissions.", path: ["expirationTime"], }) } }), ) .output(GOOGLE_DRIVE_PERMISSION_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const { data } = await getGoogleDriveApi(account.secret).permissions.create( { emailMessage: input.emailMessage, fileId: input.fileId, fields: googleDrivePermissionFields(), requestBody: { domain: input.domain, emailAddress: input.emailAddress, expirationTime: input.expirationTime, role: input.role, type: input.type, }, sendNotificationEmail: input.sendNotificationEmail, supportsAllDrives: true, }, ) return normalizeGoogleDrivePermission(data) }) export const getGoogleDrivePermission = defineAction( "Get Google Drive permission", ) .describe("Gets one permission on a file.") .account("google", GOOGLE_DRIVE_METADATA_READ_REQUIREMENT) .input(PERMISSION_ID_INPUT) .output(GOOGLE_DRIVE_PERMISSION_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const { data } = await getGoogleDriveApi(account.secret).permissions.get({ fileId: input.fileId, fields: googleDrivePermissionFields(), permissionId: input.permissionId, supportsAllDrives: true, }) return normalizeGoogleDrivePermission(data) }) export const listGoogleDrivePermissions = defineAction( "List Google Drive permissions", ) .describe("Lists permissions on a file or shared drive.") .account("google", GOOGLE_DRIVE_METADATA_READ_REQUIREMENT) .input(FILE_ID_INPUT.extend(PAGE_100_INPUT)) .output( z.object({ nextPageToken: z.string().optional(), permissions: GOOGLE_DRIVE_PERMISSION_SCHEMA.array(), }), ) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const { data } = await getGoogleDriveApi(account.secret).permissions.list({ fileId: input.fileId, fields: `nextPageToken,permissions(${googleDrivePermissionFields()})`, pageSize: input.pageSize, pageToken: input.pageToken, supportsAllDrives: true, }) return { nextPageToken: data.nextPageToken ?? undefined, permissions: (data.permissions ?? []).map(normalizeGoogleDrivePermission), } }) export const updateGoogleDrivePermission = defineAction( "Update Google Drive permission", ) .describe("Updates a permission's role or expiration.") .account("google", GOOGLE_DRIVE_WRITE_REQUIREMENT) .input( PERMISSION_ID_INPUT.extend({ expirationTime: PERMISSION_EXPIRATION_SCHEMA.nullable().optional(), role: z .enum(["organizer", "fileOrganizer", "writer", "commenter", "reader"]) .optional(), }).refine( ({ expirationTime, role }) => expirationTime !== undefined || role !== undefined, "Provide a permission property to update.", ), ) .output(GOOGLE_DRIVE_PERMISSION_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const api = getGoogleDriveApi(account.secret) if (input.expirationTime) { const { data: permission } = await api.permissions.get({ fields: "id,type", fileId: input.fileId, permissionId: input.permissionId, supportsAllDrives: true, }) if (permission.type !== "user" && permission.type !== "group") { throw new Error( "Google Drive permission expiration is available only for user and group permissions.", ) } } const { data } = await api.permissions.update({ fileId: input.fileId, fields: googleDrivePermissionFields(), permissionId: input.permissionId, removeExpiration: input.expirationTime === null, requestBody: { expirationTime: input.expirationTime ?? undefined, role: input.role, }, supportsAllDrives: true, }) return normalizeGoogleDrivePermission(data) }) export const deleteGoogleDrivePermission = defineAction( "Delete Google Drive permission", ) .describe("Deletes a permission from a Drive file.") .account("google", GOOGLE_DRIVE_WRITE_REQUIREMENT) .input(PERMISSION_ID_INPUT) .output(PERMISSION_ID_INPUT) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { await getGoogleDriveApi(account.secret).permissions.delete({ ...input, supportsAllDrives: true, }) return input }) export const createGoogleDriveComment = defineAction( "Create Google Drive comment", ) .describe("Creates a comment on a Drive file.") .account("google", GOOGLE_DRIVE_WRITE_REQUIREMENT) .input( FILE_ID_INPUT.extend({ anchor: z.string().optional(), content: z.string().min(1), }), ) .output(GOOGLE_DRIVE_COMMENT_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const { data } = await getGoogleDriveApi(account.secret).comments.create({ fileId: input.fileId, fields: googleDriveCommentFields(), requestBody: { anchor: input.anchor, content: input.content }, }) return normalizeGoogleDriveComment(data) }) export const getGoogleDriveComment = defineAction("Get Google Drive comment") .describe("Gets one Drive comment and its replies.") .account("google", GOOGLE_DRIVE_READ_REQUIREMENT) .input(COMMENT_ID_INPUT) .output(GOOGLE_DRIVE_COMMENT_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const { data } = await getGoogleDriveApi(account.secret).comments.get({ ...input, fields: googleDriveCommentFields(), includeDeleted: true, }) return normalizeGoogleDriveComment(data) }) export const listGoogleDriveComments = defineAction( "List Google Drive comments", ) .describe("Lists comments and replies on a Drive file.") .account("google", GOOGLE_DRIVE_READ_REQUIREMENT) .input( FILE_ID_INPUT.extend({ includeDeleted: z.boolean().optional(), ...PAGE_100_INPUT, startModifiedAt: z.iso.datetime({ offset: true }).optional(), }), ) .output( z.object({ comments: GOOGLE_DRIVE_COMMENT_SCHEMA.array(), nextPageToken: z.string().optional(), }), ) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const { data } = await getGoogleDriveApi(account.secret).comments.list({ fileId: input.fileId, fields: `nextPageToken,comments(${googleDriveCommentFields()})`, includeDeleted: input.includeDeleted, pageSize: input.pageSize, pageToken: input.pageToken, startModifiedTime: input.startModifiedAt, }) return { comments: (data.comments ?? []).map(normalizeGoogleDriveComment), nextPageToken: data.nextPageToken ?? undefined, } }) export const updateGoogleDriveComment = defineAction( "Update Google Drive comment", ) .describe("Updates a Drive comment's content.") .account("google", GOOGLE_DRIVE_WRITE_REQUIREMENT) .input(COMMENT_ID_INPUT.extend({ content: z.string().min(1) })) .output(GOOGLE_DRIVE_COMMENT_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const { data } = await getGoogleDriveApi(account.secret).comments.update({ commentId: input.commentId, fileId: input.fileId, fields: googleDriveCommentFields(), requestBody: { content: input.content }, }) return normalizeGoogleDriveComment(data) }) export const deleteGoogleDriveComment = defineAction( "Delete Google Drive comment", ) .describe("Deletes a comment from a Drive file.") .account("google", GOOGLE_DRIVE_WRITE_REQUIREMENT) .input(COMMENT_ID_INPUT) .output(COMMENT_ID_INPUT) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { await getGoogleDriveApi(account.secret).comments.delete(input) return input }) export const resolveGoogleDriveComment = commentStateAction( "Resolve Google Drive comment", "resolve", ) export const reopenGoogleDriveComment = commentStateAction( "Reopen Google Drive comment", "reopen", ) export const createGoogleDriveReply = defineAction("Create Google Drive reply") .describe("Replies to a Drive comment.") .account("google", GOOGLE_DRIVE_WRITE_REQUIREMENT) .input(COMMENT_ID_INPUT.extend({ content: z.string().min(1) })) .output(GOOGLE_DRIVE_REPLY_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const { data } = await getGoogleDriveApi(account.secret).replies.create({ commentId: input.commentId, fileId: input.fileId, fields: googleDriveReplyFields(), requestBody: { content: input.content }, }) return normalizeGoogleDriveReply(data) }) export const getGoogleDriveReply = defineAction("Get Google Drive reply") .describe("Gets one reply to a Drive comment.") .account("google", GOOGLE_DRIVE_READ_REQUIREMENT) .input(REPLY_ID_INPUT) .output(GOOGLE_DRIVE_REPLY_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const { data } = await getGoogleDriveApi(account.secret).replies.get({ ...input, fields: googleDriveReplyFields(), includeDeleted: true, }) return normalizeGoogleDriveReply(data) }) export const listGoogleDriveReplies = defineAction("List Google Drive replies") .describe("Lists replies to a Drive comment.") .account("google", GOOGLE_DRIVE_READ_REQUIREMENT) .input( COMMENT_ID_INPUT.extend({ includeDeleted: z.boolean().optional(), ...PAGE_100_INPUT, }), ) .output( z.object({ nextPageToken: z.string().optional(), replies: GOOGLE_DRIVE_REPLY_SCHEMA.array(), }), ) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const { data } = await getGoogleDriveApi(account.secret).replies.list({ commentId: input.commentId, fileId: input.fileId, fields: `nextPageToken,replies(${googleDriveReplyFields()})`, includeDeleted: input.includeDeleted, pageSize: input.pageSize, pageToken: input.pageToken, }) return { nextPageToken: data.nextPageToken ?? undefined, replies: (data.replies ?? []).map(normalizeGoogleDriveReply), } }) export const updateGoogleDriveReply = defineAction("Update Google Drive reply") .describe("Updates a Drive comment reply.") .account("google", GOOGLE_DRIVE_WRITE_REQUIREMENT) .input(REPLY_ID_INPUT.extend({ content: z.string().min(1) })) .output(GOOGLE_DRIVE_REPLY_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const { data } = await getGoogleDriveApi(account.secret).replies.update({ commentId: input.commentId, fileId: input.fileId, replyId: input.replyId, fields: googleDriveReplyFields(), requestBody: { content: input.content }, }) return normalizeGoogleDriveReply(data) }) export const deleteGoogleDriveReply = defineAction("Delete Google Drive reply") .describe("Deletes a reply from a Drive comment.") .account("google", GOOGLE_DRIVE_WRITE_REQUIREMENT) .input(REPLY_ID_INPUT) .output(REPLY_ID_INPUT) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { await getGoogleDriveApi(account.secret).replies.delete(input) return input }) export const createGoogleSharedDrive = defineAction( "Create Google shared drive", ) .describe("Creates a shared drive with an idempotent request ID.") .account("google", GOOGLE_DRIVE_ADMIN_REQUIREMENT) .input( z.object({ name: z.string().trim().min(1), requestId: z.uuid().optional(), }), ) .output(GOOGLE_SHARED_DRIVE_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const { data } = await getGoogleDriveApi(account.secret).drives.create({ fields: googleSharedDriveFields(), requestBody: { name: input.name }, requestId: input.requestId ?? crypto.randomUUID(), }) return normalizeGoogleSharedDrive(data) }) export const getGoogleSharedDrive = defineAction("Get Google shared drive") .describe("Gets shared-drive metadata.") .account("google", GOOGLE_SHARED_DRIVE_READ_REQUIREMENT) .input(SHARED_DRIVE_ID_INPUT) .output(GOOGLE_SHARED_DRIVE_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const { data } = await getGoogleDriveApi(account.secret).drives.get({ driveId: input.driveId, fields: googleSharedDriveFields(), }) return normalizeGoogleSharedDrive(data) }) export const listGoogleSharedDrives = defineAction("List Google shared drives") .describe("Lists shared drives available to the connected account.") .account("google", GOOGLE_SHARED_DRIVE_READ_REQUIREMENT) .input( z.object({ ...PAGE_100_INPUT, query: z.string().min(1).optional(), useDomainAdminAccess: z.boolean().optional(), }), ) .output( z.object({ drives: GOOGLE_SHARED_DRIVE_SCHEMA.array(), nextPageToken: z.string().optional(), }), ) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const { data } = await getGoogleDriveApi(account.secret).drives.list({ fields: `nextPageToken,drives(${googleSharedDriveFields()})`, pageSize: input.pageSize, pageToken: input.pageToken, q: input.query, useDomainAdminAccess: input.useDomainAdminAccess, }) return { drives: (data.drives ?? []).map(normalizeGoogleSharedDrive), nextPageToken: data.nextPageToken ?? undefined, } }) export const updateGoogleSharedDrive = defineAction( "Update Google shared drive", ) .describe("Updates a shared drive's name, theme, or color.") .account("google", GOOGLE_DRIVE_ADMIN_REQUIREMENT) .input( SHARED_DRIVE_ID_INPUT.extend({ colorRgb: z .string() .regex(/^#[\dA-Fa-f]{6}$/) .optional(), name: z.string().trim().min(1).optional(), themeId: z.string().min(1).optional(), }) .refine( ({ colorRgb, name, themeId }) => colorRgb || name || themeId, "Provide a shared-drive property to update.", ) .refine( ({ colorRgb, themeId }) => !(colorRgb && themeId), "Provide either themeId or colorRgb, not both.", ), ) .output(GOOGLE_SHARED_DRIVE_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const { data } = await getGoogleDriveApi(account.secret).drives.update({ driveId: input.driveId, fields: googleSharedDriveFields(), requestBody: { colorRgb: input.colorRgb, name: input.name, themeId: input.themeId, }, }) return normalizeGoogleSharedDrive(data) }) export const deleteGoogleSharedDrive = defineAction( "Delete Google shared drive", ) .describe("Permanently deletes an empty shared drive.") .account("google", GOOGLE_DRIVE_ADMIN_REQUIREMENT) .input(SHARED_DRIVE_ID_INPUT) .output(SHARED_DRIVE_ID_INPUT) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { await getGoogleDriveApi(account.secret).drives.delete(input) return input }) export const hideGoogleSharedDrive = sharedDriveVisibilityAction( "Hide Google shared drive", true, ) export const unhideGoogleSharedDrive = sharedDriveVisibilityAction( "Unhide Google shared drive", false, ) /** @param input - Parsed file-update input. */ function hasFileUpdate(input: z.infer) { return ( input.description !== undefined || input.name !== undefined || input.starred !== undefined ) } /** * Updates a Drive file and returns normalized metadata. * * @param secret - Refreshed Google account secret. * @param fileId - Target file ID. * @param requestBody - Provider file patch. */ async function updateFile( secret: Record, fileId: string, requestBody: drive_v3.Schema$File, ) { const { data } = await getGoogleDriveApi(secret).files.update({ fileId, fields: googleDriveFileFields(), requestBody, supportsAllDrives: true, }) return normalizeGoogleDriveFile(data) } /** * Defines a Drive trash-state action. * * @param name - Action name. * @param trashed - Desired trash state. */ function fileTrashAction(name: string, trashed: boolean) { return defineAction(name) .describe( trashed ? "Moves a Drive file to the trash." : "Restores a Drive file from the trash.", ) .account("google", GOOGLE_DRIVE_METADATA_WRITE_REQUIREMENT) .input(FILE_ID_INPUT) .output(GOOGLE_DRIVE_FILE_SCHEMA) .retry({ replaySafety: "safe" }) .handler( async ({ account, input }) => await updateFile(account.secret, input.fileId, { trashed }), ) } /** * Defines a Drive binary-content read action. * * @param name - Action name. * @param description - Action description. * @param schema - Public content-action input schema. * @param request - Provider content request. */ function fileContentAction< TInput extends { fileId: string; mimeType?: string; name?: string }, >( name: string, description: string, schema: z.ZodType, request: ( api: drive_v3.Drive, input: TInput & { mimeType: string }, ) => Promise<{ data: unknown; headers: unknown }>, ) { return defineAction(name) .describe(description) .account("google", GOOGLE_DRIVE_READ_REQUIREMENT) .input(schema) .output(z.instanceof(File)) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const response = await request(getGoogleDriveApi(account.secret), { ...input, mimeType: input.mimeType ?? "application/octet-stream", }) // Resolve the content type once across provider and caller fallbacks. const mimeType = getHeader(response.headers, "content-type") ?? input.mimeType ?? "application/octet-stream" const disposition = getHeader(response.headers, "content-disposition") // Resolve a stable File name once across explicit and provider values. const filename = input.name ?? disposition?.match(/filename="?([^";]+)"?/)?.[1] ?? input.fileId return new File([toArrayBuffer(response.data)], filename, { type: mimeType, }) }) } /** * Defines a Drive comment state transition. * * @param name - Action name. * @param action - Provider comment action. */ function commentStateAction(name: string, action: "resolve" | "reopen") { return defineAction(name) .describe( `${action === "resolve" ? "Resolves" : "Reopens"} a Drive comment thread.`, ) .account("google", GOOGLE_DRIVE_WRITE_REQUIREMENT) .input(COMMENT_ID_INPUT) .output(GOOGLE_DRIVE_REPLY_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const { data } = await getGoogleDriveApi(account.secret).replies.create({ commentId: input.commentId, fileId: input.fileId, fields: googleDriveReplyFields(), requestBody: { action }, }) return normalizeGoogleDriveReply(data) }) } /** * Defines a shared-drive visibility transition. * * @param name - Action name. * @param hidden - Desired hidden state. */ function sharedDriveVisibilityAction(name: string, hidden: boolean) { return defineAction(name) .describe( `${hidden ? "Hides" : "Restores"} a shared drive in the default view.`, ) .account("google", GOOGLE_DRIVE_ADMIN_REQUIREMENT) .input(SHARED_DRIVE_ID_INPUT) .output(GOOGLE_SHARED_DRIVE_SCHEMA) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const api = getGoogleDriveApi(account.secret) await (hidden ? api.drives.hide(input) : api.drives.unhide(input)) const { data } = await api.drives.get({ driveId: input.driveId, fields: googleSharedDriveFields(), }) return normalizeGoogleSharedDrive(data) }) } /** * Returns whether a permission expiration falls within Google's allowed window. * * @param value - RFC 3339 expiration timestamp. */ function isValidPermissionExpiration(value: string) { const now = new Date() const latest = new Date(now) latest.setUTCFullYear(latest.getUTCFullYear() + 1) const expiration = new Date(value) return expiration > now && expiration <= latest } /** * Reads a response header from supported fetch and plain-object shapes. * * @param headers - Provider response headers. * @param name - Lowercase header name. */ function getHeader(headers: unknown, name: string) { if (headers instanceof Headers) return headers.get(name) ?? undefined if (headers && typeof headers === "object" && name in headers) { const value = Object.entries(headers).find(([key]) => key === name)?.[1] return typeof value === "string" ? value : undefined } return undefined } /** * Converts supported provider content into a File-compatible buffer. * * @param value - Provider content response. * @throws When the provider returns an unsupported content type. */ function toArrayBuffer(value: unknown) { if (value instanceof ArrayBuffer) return value if (ArrayBuffer.isView(value)) return value.buffer.slice( value.byteOffset, value.byteOffset + value.byteLength, ) if (typeof value === "string") return new TextEncoder().encode(value).buffer throw new Error("Google Drive returned unsupported file content.") }