diff --git a/package-lock.json b/package-lock.json index 0f0424a..bd21431 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "@anthropic-ai/sdk": "^0.33.1", + "@automerge/automerge-repo-react-hooks": "^2.2.0", "@daily-co/daily-js": "^0.60.0", "@daily-co/daily-react": "^0.20.0", "@tldraw/assets": "^3.6.0", @@ -182,6 +183,56 @@ "undici-types": "~5.26.4" } }, + "node_modules/@automerge/automerge": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@automerge/automerge/-/automerge-3.1.1.tgz", + "integrity": "sha512-x7tZiMBLk4/SKYimVEVl1/wPntT9buGvLOWCey9ZcH8JUsB0dgm49C0S7Ojzgvflcs2hc/YjiXRPcFeFkinIgw==", + "license": "MIT" + }, + "node_modules/@automerge/automerge-repo": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@automerge/automerge-repo/-/automerge-repo-2.2.0.tgz", + "integrity": "sha512-/4cAxnUDPykqdSMJJiJvPlR2VY0JHJnvEoM7fO8qVXwQors34S0VkJEScXRJZExcVhWGhFTwCTWvUbb6hhWQIQ==", + "license": "MIT", + "dependencies": { + "@automerge/automerge": "2.2.8 - 3", + "bs58check": "^3.0.1", + "cbor-x": "^1.3.0", + "debug": "^4.3.4", + "eventemitter3": "^5.0.1", + "fast-sha256": "^1.3.0", + "uuid": "^9.0.0", + "xstate": "^5.9.1" + } + }, + "node_modules/@automerge/automerge-repo-react-hooks": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@automerge/automerge-repo-react-hooks/-/automerge-repo-react-hooks-2.2.0.tgz", + "integrity": "sha512-zHP44jXSCmV1DyyKdTKgv8nz7yrdCT7VFXs/QrF3YRhE/3czhhvbXo2zahGvOoM+jryH0eJxHTrJ7jMEtt/Ung==", + "license": "MIT", + "dependencies": { + "@automerge/automerge": "2.2.8 - 3", + "@automerge/automerge-repo": "2.2.0", + "eventemitter3": "^5.0.1", + "react-usestateref": "^1.0.8" + }, + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, + "node_modules/@automerge/automerge-repo-react-hooks/node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, + "node_modules/@automerge/automerge-repo/node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, "node_modules/@babel/code-frame": { "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", @@ -473,6 +524,84 @@ "license": "MIT", "optional": true }, + "node_modules/@cbor-extract/cbor-extract-darwin-arm64": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-arm64/-/cbor-extract-darwin-arm64-2.2.0.tgz", + "integrity": "sha512-P7swiOAdF7aSi0H+tHtHtr6zrpF3aAq/W9FXx5HektRvLTM2O89xCyXF3pk7pLc7QpaY7AoaE8UowVf9QBdh3w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@cbor-extract/cbor-extract-darwin-x64": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-darwin-x64/-/cbor-extract-darwin-x64-2.2.0.tgz", + "integrity": "sha512-1liF6fgowph0JxBbYnAS7ZlqNYLf000Qnj4KjqPNW4GViKrEql2MgZnAsExhY9LSy8dnvA4C0qHEBgPrll0z0w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@cbor-extract/cbor-extract-linux-arm": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-arm/-/cbor-extract-linux-arm-2.2.0.tgz", + "integrity": "sha512-QeBcBXk964zOytiedMPQNZr7sg0TNavZeuUCD6ON4vEOU/25+pLhNN6EDIKJ9VLTKaZ7K7EaAriyYQ1NQ05s/Q==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@cbor-extract/cbor-extract-linux-arm64": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-arm64/-/cbor-extract-linux-arm64-2.2.0.tgz", + "integrity": "sha512-rQvhNmDuhjTVXSPFLolmQ47/ydGOFXtbR7+wgkSY0bdOxCFept1hvg59uiLPT2fVDuJFuEy16EImo5tE2x3RsQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@cbor-extract/cbor-extract-linux-x64": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-linux-x64/-/cbor-extract-linux-x64-2.2.0.tgz", + "integrity": "sha512-cWLAWtT3kNLHSvP4RKDzSTX9o0wvQEEAj4SKvhWuOVZxiDAeQazr9A+PSiRILK1VYMLeDml89ohxCnUNQNQNCw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@cbor-extract/cbor-extract-win32-x64": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cbor-extract/cbor-extract-win32-x64/-/cbor-extract-win32-x64-2.2.0.tgz", + "integrity": "sha512-l2M+Z8DO2vbvADOBNLbbh9y5ST1RY5sqkWOg/58GkUPBYou/cuNZ68SGQ644f1CvZ8kcOxyZtw06+dxWHIoN/w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@cloudflare/intl-types": { "version": "1.5.6", "resolved": "https://registry.npmjs.org/@cloudflare/intl-types/-/intl-types-1.5.6.tgz", @@ -1838,6 +1967,18 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "license": "ISC" }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -4400,6 +4541,12 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/base-x": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-4.0.1.tgz", + "integrity": "sha512-uAZ8x6r6S3aUM9rbHGVOIsR15U/ZSc82b3ymnCPsT45Gk1DDvhDPdIgB5MrhirZWt+5K0EEPQH985kNqZgNPFw==", + "license": "MIT" + }, "node_modules/base64-arraybuffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", @@ -4508,6 +4655,25 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bs58": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/bs58/-/bs58-5.0.0.tgz", + "integrity": "sha512-r+ihvQJvahgYT50JD05dyJNKlmmSlMoOGwn1lCcEzanPglg7TxYjioQUYehQ9mAR/+hOSd2jRc/Z2y5UxBymvQ==", + "license": "MIT", + "dependencies": { + "base-x": "^4.0.0" + } + }, + "node_modules/bs58check": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/bs58check/-/bs58check-3.0.1.tgz", + "integrity": "sha512-hjuuJvoWEybo7Hn/0xOrczQKKEKD63WguEjlhLExYs2wUBcebDC1jDNK17eEAD2lYfw82d5ASC1d7K3SWszjaQ==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "^1.2.0", + "bs58": "^5.0.0" + } + }, "node_modules/btoa": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz", @@ -4608,6 +4774,37 @@ "license": "MIT", "optional": true }, + "node_modules/cbor-extract": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cbor-extract/-/cbor-extract-2.2.0.tgz", + "integrity": "sha512-Ig1zM66BjLfTXpNgKpvBePq271BPOvu8MR0Jl080yG7Jsl+wAZunfrwiwA+9ruzm/WEdIV5QF/bjDZTqyAIVHA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.1.1" + }, + "bin": { + "download-cbor-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@cbor-extract/cbor-extract-darwin-arm64": "2.2.0", + "@cbor-extract/cbor-extract-darwin-x64": "2.2.0", + "@cbor-extract/cbor-extract-linux-arm": "2.2.0", + "@cbor-extract/cbor-extract-linux-arm64": "2.2.0", + "@cbor-extract/cbor-extract-linux-x64": "2.2.0", + "@cbor-extract/cbor-extract-win32-x64": "2.2.0" + } + }, + "node_modules/cbor-x": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/cbor-x/-/cbor-x-1.6.0.tgz", + "integrity": "sha512-0kareyRwHSkL6ws5VXHEf8uY1liitysCVJjlmhaLG+IXLqhSaOO+t63coaso7yjwEzWZzLy8fJo06gZDVQM9Qg==", + "license": "MIT", + "optionalDependencies": { + "cbor-extract": "^2.2.0" + } + }, "node_modules/ccount": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", @@ -6530,6 +6727,12 @@ "license": "MIT", "optional": true }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "license": "Unlicense" + }, "node_modules/fastq": { "version": "1.18.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz", @@ -9155,6 +9358,21 @@ "node-gyp-build-test": "build-test.js" } }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.1.1.tgz", + "integrity": "sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==", + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, "node_modules/node-releases": { "version": "2.0.19", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", @@ -9866,6 +10084,15 @@ } } }, + "node_modules/react-usestateref": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/react-usestateref/-/react-usestateref-1.0.9.tgz", + "integrity": "sha512-t8KLsI7oje0HzfzGhxFXzuwbf1z9vhBM1ptHLUIHhYqZDKFuI5tzdhEVxSNzUkYxwF8XdpOErzHlKxvP7sTERw==", + "license": "ISC", + "peerDependencies": { + "react": ">16.0.0" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -11526,7 +11753,6 @@ "https://github.com/sponsors/ctavan" ], "license": "MIT", - "optional": true, "bin": { "uuid": "dist/bin/uuid" } @@ -12457,6 +12683,16 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "license": "MIT" }, + "node_modules/xstate": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/xstate/-/xstate-5.20.1.tgz", + "integrity": "sha512-i9ZpNnm/XhCOMUxae1suT8PjYNTStZWbhmuKt4xeTPaYG5TS0Fz0i+Ka5yxoNPpaHW3VW6JIowrwFgSTZONxig==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/xstate" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 6fe2cdc..17fe5ed 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "license": "ISC", "dependencies": { "@anthropic-ai/sdk": "^0.33.1", + "@automerge/automerge-repo-react-hooks": "^2.2.0", "@daily-co/daily-js": "^0.60.0", "@daily-co/daily-react": "^0.20.0", "@tldraw/assets": "^3.6.0", diff --git a/src/routes/Board.tsx b/src/routes/Board.tsx index 0a1581a..8351194 100644 --- a/src/routes/Board.tsx +++ b/src/routes/Board.tsx @@ -29,6 +29,8 @@ import { SlideShape } from "@/shapes/SlideShapeUtil" import { makeRealSettings, applySettingsMigrations } from "@/lib/settings" import { PromptShapeTool } from "@/tools/PromptShapeTool" import { PromptShape } from "@/shapes/PromptShapeUtil" +import { SharedPianoTool } from "@/tools/SharedPianoTool" +import { SharedPianoShape } from "@/shapes/SharedPianoShapeUtil" import { lockElement, unlockElement, @@ -58,6 +60,7 @@ const customShapeUtils = [ MycrozineTemplateShape, MarkdownShape, PromptShape, + SharedPianoShape, ] const customTools = [ ChatBoxTool, @@ -67,6 +70,7 @@ const customTools = [ MycrozineTemplateTool, MarkdownTool, PromptShapeTool, + SharedPianoTool, GestureTool, ] diff --git a/src/shapes/SharedPianoShapeUtil.tsx b/src/shapes/SharedPianoShapeUtil.tsx new file mode 100644 index 0000000..91bfa47 --- /dev/null +++ b/src/shapes/SharedPianoShapeUtil.tsx @@ -0,0 +1,257 @@ +import { BaseBoxShapeUtil, TLBaseShape } from "tldraw" +import { useCallback, useState } from "react" + +export type ISharedPianoShape = TLBaseShape< + "SharedPiano", + { + w: number + h: number + isMinimized?: boolean + interactionState?: { + scrollPosition?: { x: number; y: number } + } + } +> + +const getDefaultDimensions = (): { w: number; h: number } => { + // Default dimensions for the Shared Piano (16:9 ratio) + return { w: 800, h: 600 } +} + +export class SharedPianoShape extends BaseBoxShapeUtil { + static override type = "SharedPiano" + + getDefaultProps(): ISharedPianoShape["props"] { + const { w, h } = getDefaultDimensions() + return { + w, + h, + isMinimized: false, + } + } + + indicator(shape: ISharedPianoShape) { + return ( + + ) + } + + component(shape: ISharedPianoShape) { + const [isLoading, setIsLoading] = useState(true) + const [error, setError] = useState(null) + + const handleIframeLoad = useCallback(() => { + setIsLoading(false) + setError(null) + }, []) + + const handleIframeError = useCallback(() => { + setIsLoading(false) + setError("Failed to load Shared Piano") + }, []) + + const handleToggleMinimize = (e: React.MouseEvent) => { + e.stopPropagation() + this.editor.updateShape({ + id: shape.id, + type: "SharedPiano", + props: { + ...shape.props, + isMinimized: !shape.props.isMinimized, + }, + }) + } + + const controls = ( +
+ +
+ ) + + const sharedPianoUrl = "https://musiclab.chromeexperiments.com/Shared-Piano/#jQB715bFJ" + + return ( +
+ {controls} + + {shape.props.isMinimized ? ( +
+ ๐ŸŽน Shared Piano +
+ ) : ( + <> + {isLoading && ( +
+
+
๐ŸŽน
+
Loading Shared Piano...
+
+
+ )} + + {error && ( +
+
+
โŒ
+
{error}
+ +
+
+ )} + + + {/* Recording Button */} {shape.props.enableRecording && ( )} + {/* Test Button - Always visible for debugging */} + + + {/* Transcription Button - Only for owners */} + {(() => { + console.log('๐Ÿ” Checking transcription button conditions:'); + console.log('enableTranscription:', shape.props.enableTranscription); + console.log('isOwner:', shape.props.isOwner); + console.log('Button should render:', shape.props.enableTranscription && shape.props.isOwner); + return shape.props.enableTranscription && shape.props.isOwner; + })() && ( + + )} + + {/* Transcription History */} + {shape.props.transcriptionHistory.length > 0 && ( +
+
+ Live Transcription: +
+ {shape.props.transcriptionHistory.slice(-10).map((msg) => ( +
+ + {msg.sender}: + {" "} + {msg.message} +
+ ))} +
+ )} +

{ }} > url: {roomUrl} + {shape.props.isOwner && " (Owner)"}

) diff --git a/src/tools/SharedPianoTool.ts b/src/tools/SharedPianoTool.ts new file mode 100644 index 0000000..f3191e8 --- /dev/null +++ b/src/tools/SharedPianoTool.ts @@ -0,0 +1,7 @@ +import { BaseBoxShapeTool } from "tldraw" + +export class SharedPianoTool extends BaseBoxShapeTool { + static override id = "SharedPiano" + shapeType = "SharedPiano" + override initial = "idle" +} \ No newline at end of file diff --git a/src/ui/CustomToolbar.tsx b/src/ui/CustomToolbar.tsx index 8b98576..b984641 100644 --- a/src/ui/CustomToolbar.tsx +++ b/src/ui/CustomToolbar.tsx @@ -168,6 +168,14 @@ export function CustomToolbar() { isSelected={tools["Prompt"].id === editor.getCurrentToolId()} /> )} + {tools["SharedPiano"] && ( + + )} ) diff --git a/src/ui/overrides.tsx b/src/ui/overrides.tsx index 84f4ddc..1c42d86 100644 --- a/src/ui/overrides.tsx +++ b/src/ui/overrides.tsx @@ -151,6 +151,15 @@ export const overrides: TLUiOverrides = { readonlyOk: true, onSelect: () => editor.setCurrentTool("Prompt"), }, + SharedPiano: { + id: "SharedPiano", + icon: "music", + label: "Shared Piano", + type: "SharedPiano", + kbd: "alt+p", + readonlyOk: true, + onSelect: () => editor.setCurrentTool("SharedPiano"), + }, gesture: { id: "gesture", icon: "draw", diff --git a/worker/TldrawDurableObject.ts b/worker/TldrawDurableObject.ts index add19fe..5869c06 100644 --- a/worker/TldrawDurableObject.ts +++ b/worker/TldrawDurableObject.ts @@ -19,6 +19,7 @@ import { MarkdownShape } from "@/shapes/MarkdownShapeUtil" import { MycrozineTemplateShape } from "@/shapes/MycrozineTemplateShapeUtil" import { SlideShape } from "@/shapes/SlideShapeUtil" import { PromptShape } from "@/shapes/PromptShapeUtil" +import { SharedPianoShape } from "@/shapes/SharedPianoShapeUtil" // add custom shapes and bindings here if needed: export const customSchema = createTLSchema({ @@ -52,6 +53,10 @@ export const customSchema = createTLSchema({ props: PromptShape.props, migrations: PromptShape.migrations, }, + SharedPiano: { + props: SharedPianoShape.props, + migrations: SharedPianoShape.migrations, + }, }, bindings: defaultBindingSchemas, }) diff --git a/worker/worker.ts b/worker/worker.ts index ac2d0f0..b60d760 100644 --- a/worker/worker.ts +++ b/worker/worker.ts @@ -162,6 +162,50 @@ const router = AutoRouter({ } }) + .post("/daily/tokens", async (req) => { + const apiKey = req.headers.get('Authorization')?.split('Bearer ')[1] + + if (!apiKey) { + return new Response(JSON.stringify({ error: 'No API key provided' }), { + status: 401, + headers: { 'Content-Type': 'application/json' } + }) + } + + try { + const body = await req.json() + const response = await fetch('https://api.daily.co/v1/meeting-tokens', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${apiKey}` + }, + body: JSON.stringify({ + room_name: body.room_name, + properties: body.properties + }) + }) + + if (!response.ok) { + const error = await response.json() + return new Response(JSON.stringify(error), { + status: response.status, + headers: { 'Content-Type': 'application/json' } + }) + } + + const data = await response.json() + return new Response(JSON.stringify(data), { + headers: { 'Content-Type': 'application/json' } + }) + } catch (error) { + return new Response(JSON.stringify({ error: (error as Error).message }), { + status: 500, + headers: { 'Content-Type': 'application/json' } + }) + } + }) + // Add new transcription endpoints .post("/daily/rooms/:roomName/start-transcription", async (req) => { const apiKey = req.headers.get('Authorization')?.split('Bearer ')[1] @@ -325,6 +369,88 @@ const router = AutoRouter({ } }) + // Recording endpoints + .post("/daily/recordings/start", async (req) => { + const apiKey = req.headers.get('Authorization')?.split('Bearer ')[1] + + if (!apiKey) { + return new Response(JSON.stringify({ error: 'No API key provided' }), { + status: 401, + headers: { 'Content-Type': 'application/json' } + }) + } + + try { + const body = await req.json() + const response = await fetch('https://api.daily.co/v1/recordings', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${apiKey}` + }, + body: JSON.stringify(body) + }) + + if (!response.ok) { + const error = await response.json() + return new Response(JSON.stringify(error), { + status: response.status, + headers: { 'Content-Type': 'application/json' } + }) + } + + const data = await response.json() + return new Response(JSON.stringify(data), { + headers: { 'Content-Type': 'application/json' } + }) + } catch (error) { + return new Response(JSON.stringify({ error: (error as Error).message }), { + status: 500, + headers: { 'Content-Type': 'application/json' } + }) + } + }) + + .post("/daily/recordings/:recordingId/stop", async (req) => { + const apiKey = req.headers.get('Authorization')?.split('Bearer ')[1] + const { recordingId } = req.params + + if (!apiKey) { + return new Response(JSON.stringify({ error: 'No API key provided' }), { + status: 401, + headers: { 'Content-Type': 'application/json' } + }) + } + + try { + const response = await fetch(`https://api.daily.co/v1/recordings/${recordingId}/stop`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${apiKey}` + } + }) + + if (!response.ok) { + const error = await response.json() + return new Response(JSON.stringify(error), { + status: response.status, + headers: { 'Content-Type': 'application/json' } + }) + } + + const data = await response.json() + return new Response(JSON.stringify(data), { + headers: { 'Content-Type': 'application/json' } + }) + } catch (error) { + return new Response(JSON.stringify({ error: (error as Error).message }), { + status: 500, + headers: { 'Content-Type': 'application/json' } + }) + } + }) + async function backupAllBoards(env: Environment) { try { // List all room files from TLDRAW_BUCKET