videochat tool update
This commit is contained in:
parent
0259ae4149
commit
fb3a525340
|
|
@ -15,6 +15,8 @@
|
||||||
"author": "Jeff Emmett",
|
"author": "Jeff Emmett",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@daily-co/daily-js": "^0.60.0",
|
||||||
|
"@daily-co/daily-react": "^0.20.0",
|
||||||
"@tldraw/assets": "^3.6.0",
|
"@tldraw/assets": "^3.6.0",
|
||||||
"@tldraw/sync": "^3.6.0",
|
"@tldraw/sync": "^3.6.0",
|
||||||
"@tldraw/sync-core": "^3.6.0",
|
"@tldraw/sync-core": "^3.6.0",
|
||||||
|
|
@ -26,11 +28,13 @@
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
"html2canvas": "^1.4.1",
|
"html2canvas": "^1.4.1",
|
||||||
"itty-router": "^5.0.17",
|
"itty-router": "^5.0.17",
|
||||||
|
"jotai": "^2.6.0",
|
||||||
"jspdf": "^2.5.2",
|
"jspdf": "^2.5.2",
|
||||||
"lodash.throttle": "^4.1.1",
|
"lodash.throttle": "^4.1.1",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router-dom": "^7.0.2",
|
"react-router-dom": "^7.0.2",
|
||||||
|
"recoil": "^0.7.7",
|
||||||
"tldraw": "^3.6.0",
|
"tldraw": "^3.6.0",
|
||||||
"vercel": "^39.1.1"
|
"vercel": "^39.1.1"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -53,19 +53,25 @@ export function useCameraControls(editor: Editor | null) {
|
||||||
const frameId = searchParams.get("frameId")
|
const frameId = searchParams.get("frameId")
|
||||||
const isLocked = searchParams.get("isLocked") === "true"
|
const isLocked = searchParams.get("isLocked") === "true"
|
||||||
|
|
||||||
console.log("Setting camera:", { x, y, zoom })
|
console.log("Setting camera:", { x, y, zoom, frameId, isLocked })
|
||||||
|
|
||||||
// Set camera position if coordinates exist
|
// Set camera position if coordinates exist
|
||||||
if (x && y && zoom) {
|
if (x && y && zoom) {
|
||||||
const position = {
|
const position = {
|
||||||
x: parseFloat(x),
|
x: Math.round(parseFloat(x)),
|
||||||
y: parseFloat(y),
|
y: Math.round(parseFloat(y)),
|
||||||
z: parseFloat(zoom),
|
z: Math.round(parseFloat(zoom)),
|
||||||
}
|
}
|
||||||
console.log("Camera position:", position)
|
console.log("Camera position:", position)
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
editor.setCamera(position, { animation: { duration: 0 } })
|
editor.setCamera(position, { animation: { duration: 0 } })
|
||||||
|
|
||||||
|
// Apply camera lock immediately after setting position if needed
|
||||||
|
if (isLocked) {
|
||||||
|
editor.setCameraOptions({ isLocked: true })
|
||||||
|
}
|
||||||
|
|
||||||
console.log("Current camera:", editor.getCamera())
|
console.log("Current camera:", editor.getCamera())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -78,9 +84,17 @@ export function useCameraControls(editor: Editor | null) {
|
||||||
|
|
||||||
// If x/y/zoom are not provided in URL, zoom to frame bounds
|
// If x/y/zoom are not provided in URL, zoom to frame bounds
|
||||||
if (!x || !y || !zoom) {
|
if (!x || !y || !zoom) {
|
||||||
editor.zoomToBounds(editor.getShapePageBounds(frame)!, {
|
const bounds = editor.getShapePageBounds(frame)!
|
||||||
|
const viewportPageBounds = editor.getViewportPageBounds()
|
||||||
|
const targetZoom = Math.min(
|
||||||
|
viewportPageBounds.width / bounds.width,
|
||||||
|
viewportPageBounds.height / bounds.height,
|
||||||
|
1, // Cap at 1x zoom, matching lockCameraToFrame
|
||||||
|
)
|
||||||
|
|
||||||
|
editor.zoomToBounds(bounds, {
|
||||||
animation: { duration: 0 },
|
animation: { duration: 0 },
|
||||||
targetZoom: 1,
|
targetZoom,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -129,9 +143,9 @@ export function useCameraControls(editor: Editor | null) {
|
||||||
if (!editor) return
|
if (!editor) return
|
||||||
const camera = editor.getCamera()
|
const camera = editor.getCamera()
|
||||||
const url = new URL(window.location.href)
|
const url = new URL(window.location.href)
|
||||||
url.searchParams.set("x", camera.x.toString())
|
url.searchParams.set("x", Math.round(camera.x).toString())
|
||||||
url.searchParams.set("y", camera.y.toString())
|
url.searchParams.set("y", Math.round(camera.y).toString())
|
||||||
url.searchParams.set("zoom", camera.z.toString())
|
url.searchParams.set("zoom", Math.round(camera.z).toString())
|
||||||
navigator.clipboard.writeText(url.toString())
|
navigator.clipboard.writeText(url.toString())
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { BaseBoxShapeUtil, TLBaseShape } from "tldraw"
|
import { BaseBoxShapeUtil, TLBaseShape } from "tldraw"
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState, useRef } from "react"
|
||||||
import { WORKER_URL } from "../routes/Board"
|
import { WORKER_URL } from "../routes/Board"
|
||||||
|
import DailyIframe from "@daily-co/daily-js"
|
||||||
|
|
||||||
export type IVideoChatShape = TLBaseShape<
|
export type IVideoChatShape = TLBaseShape<
|
||||||
"VideoChat",
|
"VideoChat",
|
||||||
|
|
@ -12,6 +13,53 @@ export type IVideoChatShape = TLBaseShape<
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|
||||||
|
// Simplified component using Daily Prebuilt
|
||||||
|
const VideoChatComponent = ({ roomUrl }: { roomUrl: string }) => {
|
||||||
|
const wrapperRef = useRef<HTMLDivElement>(null)
|
||||||
|
const callFrameRef = useRef<ReturnType<
|
||||||
|
typeof DailyIframe.createFrame
|
||||||
|
> | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!wrapperRef.current || !roomUrl) return
|
||||||
|
|
||||||
|
// Create and configure the Daily call frame
|
||||||
|
callFrameRef.current = DailyIframe.createFrame(wrapperRef.current, {
|
||||||
|
iframeStyle: {
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
border: "0",
|
||||||
|
borderRadius: "4px",
|
||||||
|
},
|
||||||
|
showLeaveButton: true,
|
||||||
|
showFullscreenButton: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Join the room
|
||||||
|
callFrameRef.current.join({ url: roomUrl })
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
return () => {
|
||||||
|
if (callFrameRef.current) {
|
||||||
|
callFrameRef.current.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [roomUrl])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={wrapperRef}
|
||||||
|
style={{
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
backgroundColor: "#f0f0f0",
|
||||||
|
borderRadius: "4px",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||||
static override type = "VideoChat"
|
static override type = "VideoChat"
|
||||||
|
|
||||||
|
|
@ -33,29 +81,37 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(`${WORKER_URL}/daily/rooms`, {
|
try {
|
||||||
method: "POST",
|
const response = await fetch(`${WORKER_URL}/daily/rooms`, {
|
||||||
headers: {
|
method: "POST",
|
||||||
"Content-Type": "application/json",
|
headers: {
|
||||||
},
|
"Content-Type": "application/json",
|
||||||
body: JSON.stringify({
|
|
||||||
properties: {
|
|
||||||
enable_recording: true,
|
|
||||||
max_participants: 8,
|
|
||||||
},
|
},
|
||||||
}),
|
body: JSON.stringify({
|
||||||
})
|
properties: {
|
||||||
|
enable_recording: true,
|
||||||
|
max_participants: 8,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
const data = await response.json()
|
if (!response.ok) {
|
||||||
|
throw new Error("Failed to create room")
|
||||||
|
}
|
||||||
|
|
||||||
this.editor.updateShape<IVideoChatShape>({
|
const data = await response.json()
|
||||||
id: shape.id,
|
|
||||||
type: "VideoChat",
|
this.editor.updateShape<IVideoChatShape>({
|
||||||
props: {
|
id: shape.id,
|
||||||
...shape.props,
|
type: "VideoChat",
|
||||||
roomUrl: (data as any).url,
|
props: {
|
||||||
},
|
...shape.props,
|
||||||
})
|
roomUrl: (data as any).url,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to create Daily room:", error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
component(shape: IVideoChatShape) {
|
component(shape: IVideoChatShape) {
|
||||||
|
|
@ -64,60 +120,91 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isInRoom && shape.props.roomUrl) {
|
setIsLoading(true)
|
||||||
const script = document.createElement("script")
|
this.ensureRoomExists(shape)
|
||||||
script.src = "https://www.daily.co/static/call-machine.js"
|
.catch((err) => setError(err.message))
|
||||||
document.body.appendChild(script)
|
.finally(() => setIsLoading(false))
|
||||||
|
}, [])
|
||||||
|
|
||||||
script.onload = () => {
|
if (isLoading) {
|
||||||
// @ts-ignore
|
return (
|
||||||
window.DailyIframe.createFrame({
|
<div
|
||||||
iframeStyle: {
|
style={{
|
||||||
width: "100%",
|
width: `${shape.props.w}px`,
|
||||||
height: "100%",
|
height: `${shape.props.h}px`,
|
||||||
border: "0",
|
display: "flex",
|
||||||
borderRadius: "4px",
|
alignItems: "center",
|
||||||
},
|
justifyContent: "center",
|
||||||
showLeaveButton: true,
|
backgroundColor: "#f0f0f0",
|
||||||
showFullscreenButton: true,
|
borderRadius: "4px",
|
||||||
}).join({ url: shape.props.roomUrl })
|
}}
|
||||||
}
|
>
|
||||||
}
|
<div>Initializing video chat...</div>
|
||||||
}, [isInRoom, shape.props.roomUrl])
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!shape.props.roomUrl) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
width: `${shape.props.w}px`,
|
||||||
|
height: `${shape.props.h}px`,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
backgroundColor: "#f0f0f0",
|
||||||
|
borderRadius: "4px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Creating room...
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
pointerEvents: "all",
|
|
||||||
width: `${shape.props.w}px`,
|
width: `${shape.props.w}px`,
|
||||||
height: `${shape.props.h}px`,
|
height: `${shape.props.h}px`,
|
||||||
position: "absolute",
|
position: "relative",
|
||||||
top: "10px",
|
|
||||||
left: "10px",
|
|
||||||
zIndex: 9999,
|
|
||||||
padding: "15px",
|
|
||||||
backgroundColor: "#F0F0F0",
|
|
||||||
boxShadow: "0 2px 4px rgba(0, 0, 0, 0.1)",
|
|
||||||
borderRadius: "4px",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{!isInRoom ? (
|
{!isInRoom ? (
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsInRoom(true)}
|
onClick={() => setIsInRoom(true)}
|
||||||
className="bg-blue-500 text-white px-4 py-2 rounded"
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "50%",
|
||||||
|
left: "50%",
|
||||||
|
transform: "translate(-50%, -50%)",
|
||||||
|
padding: "12px 24px",
|
||||||
|
backgroundColor: "#2563eb",
|
||||||
|
color: "white",
|
||||||
|
border: "none",
|
||||||
|
borderRadius: "4px",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Join Room
|
Join Video Chat
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<div
|
<VideoChatComponent roomUrl={shape.props.roomUrl} />
|
||||||
id="daily-call-iframe-container"
|
)}
|
||||||
style={{
|
{error && (
|
||||||
width: "100%",
|
<div
|
||||||
height: "100%",
|
style={{
|
||||||
}}
|
position: "absolute",
|
||||||
/>
|
bottom: 10,
|
||||||
|
left: 10,
|
||||||
|
right: 10,
|
||||||
|
color: "red",
|
||||||
|
textAlign: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{error && <p className="text-red-500 mt-2">{error}</p>}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -186,7 +186,7 @@ export const lockCameraToFrame = async (editor: Editor) => {
|
||||||
const baseUrl = `${window.location.origin}${window.location.pathname}`
|
const baseUrl = `${window.location.origin}${window.location.pathname}`
|
||||||
const url = new URL(baseUrl)
|
const url = new URL(baseUrl)
|
||||||
|
|
||||||
// Calculate zoom level to fit the frame
|
// Calculate zoom level to fit the frame (for URL only)
|
||||||
const viewportPageBounds = editor.getViewportPageBounds()
|
const viewportPageBounds = editor.getViewportPageBounds()
|
||||||
const targetZoom = Math.min(
|
const targetZoom = Math.min(
|
||||||
viewportPageBounds.width / bounds.width,
|
viewportPageBounds.width / bounds.width,
|
||||||
|
|
@ -194,17 +194,19 @@ export const lockCameraToFrame = async (editor: Editor) => {
|
||||||
1, // Cap at 1x zoom
|
1, // Cap at 1x zoom
|
||||||
)
|
)
|
||||||
|
|
||||||
// Set camera parameters first
|
// Set URL parameters without affecting the current view
|
||||||
url.searchParams.set("x", Math.round(bounds.x).toString())
|
url.searchParams.set("x", Math.round(bounds.x).toString())
|
||||||
url.searchParams.set("y", Math.round(bounds.y).toString())
|
url.searchParams.set("y", Math.round(bounds.y).toString())
|
||||||
url.searchParams.set("zoom", targetZoom.toString())
|
url.searchParams.set(
|
||||||
|
"zoom",
|
||||||
// Add frame-specific parameters last
|
(Math.round(targetZoom * 100) / 100).toString(),
|
||||||
url.searchParams.set("isLocked", "true")
|
)
|
||||||
url.searchParams.set("frameId", selectedShape.id)
|
url.searchParams.set("frameId", selectedShape.id)
|
||||||
|
url.searchParams.set("isLocked", "true")
|
||||||
|
|
||||||
const finalUrl = url.toString()
|
const finalUrl = url.toString()
|
||||||
|
|
||||||
|
// Copy URL to clipboard without modifying the current view
|
||||||
if (navigator.clipboard && window.isSecureContext) {
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
await navigator.clipboard.writeText(finalUrl)
|
await navigator.clipboard.writeText(finalUrl)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
263
worker/worker.ts
263
worker/worker.ts
|
|
@ -1,135 +1,182 @@
|
||||||
import { handleUnfurlRequest } from 'cloudflare-workers-unfurl'
|
import { handleUnfurlRequest } from "cloudflare-workers-unfurl"
|
||||||
import { AutoRouter, cors, error, IRequest } from 'itty-router'
|
import { AutoRouter, cors, error, IRequest } from "itty-router"
|
||||||
import { handleAssetDownload, handleAssetUpload } from './assetUploads'
|
import { handleAssetDownload, handleAssetUpload } from "./assetUploads"
|
||||||
import { Environment } from './types'
|
import { Environment } from "./types"
|
||||||
|
|
||||||
// make sure our sync durable object is made available to cloudflare
|
// make sure our sync durable object is made available to cloudflare
|
||||||
export { TldrawDurableObject } from './TldrawDurableObject'
|
export { TldrawDurableObject } from "./TldrawDurableObject"
|
||||||
|
|
||||||
// Define security headers
|
// Define security headers
|
||||||
const securityHeaders = {
|
const securityHeaders = {
|
||||||
'Content-Security-Policy': "default-src 'self'; connect-src 'self' wss: https:; img-src 'self' data: blob: https:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';",
|
"Content-Security-Policy":
|
||||||
'X-Content-Type-Options': 'nosniff',
|
"default-src 'self'; connect-src 'self' wss: https:; img-src 'self' data: blob: https:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';",
|
||||||
'X-Frame-Options': 'DENY',
|
"X-Content-Type-Options": "nosniff",
|
||||||
'X-XSS-Protection': '1; mode=block',
|
"X-Frame-Options": "DENY",
|
||||||
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
|
"X-XSS-Protection": "1; mode=block",
|
||||||
'Referrer-Policy': 'strict-origin-when-cross-origin',
|
"Strict-Transport-Security": "max-age=31536000; includeSubDomains",
|
||||||
'Permissions-Policy': 'camera=(), microphone=(), geolocation=()'
|
"Referrer-Policy": "strict-origin-when-cross-origin",
|
||||||
|
"Permissions-Policy": "camera=(), microphone=(), geolocation=()",
|
||||||
}
|
}
|
||||||
|
|
||||||
// we use itty-router (https://itty.dev/) to handle routing. in this example we turn on CORS because
|
// we use itty-router (https://itty.dev/) to handle routing. in this example we turn on CORS because
|
||||||
// we're hosting the worker separately to the client. you should restrict this to your own domain.
|
// we're hosting the worker separately to the client. you should restrict this to your own domain.
|
||||||
const { preflight, corsify } = cors({
|
const { preflight, corsify } = cors({
|
||||||
origin: (origin) => {
|
origin: (origin) => {
|
||||||
const allowedOrigins = [
|
const allowedOrigins = [
|
||||||
'https://jeffemmett.com',
|
"https://jeffemmett.com",
|
||||||
'https://www.jeffemmett.com',
|
"https://www.jeffemmett.com",
|
||||||
'https://jeffemmett-canvas.jeffemmett.workers.dev',
|
"https://jeffemmett-canvas.jeffemmett.workers.dev",
|
||||||
'https://jeffemmett.com/board/*',
|
"https://jeffemmett.com/board/*",
|
||||||
];
|
]
|
||||||
|
|
||||||
// Always allow if no origin (like from a local file)
|
// Always allow if no origin (like from a local file)
|
||||||
if (!origin) return '*';
|
if (!origin) return "*"
|
||||||
|
|
||||||
// Check exact matches
|
// Check exact matches
|
||||||
if (allowedOrigins.includes(origin)) {
|
if (allowedOrigins.includes(origin)) {
|
||||||
return origin;
|
return origin
|
||||||
}
|
}
|
||||||
|
|
||||||
// For development - check if it's a localhost or local IP
|
// For development - check if it's a localhost or local IP
|
||||||
if (origin.match(/^http:\/\/(localhost|127\.0\.0\.192\.168\.|169\.254\.|10\.)/)) {
|
if (
|
||||||
return origin;
|
origin.match(
|
||||||
}
|
/^http:\/\/(localhost|127\.0\.0\.192\.168\.|169\.254\.|10\.)/,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return origin
|
||||||
|
}
|
||||||
|
|
||||||
return undefined;
|
return undefined
|
||||||
},
|
},
|
||||||
allowMethods: ['GET', 'POST', 'OPTIONS', 'UPGRADE'],
|
allowMethods: ["GET", "POST", "OPTIONS", "UPGRADE"],
|
||||||
allowHeaders: [
|
allowHeaders: [
|
||||||
'Content-Type',
|
"Content-Type",
|
||||||
'Authorization',
|
"Authorization",
|
||||||
'Upgrade',
|
"Upgrade",
|
||||||
'Connection',
|
"Connection",
|
||||||
'Sec-WebSocket-Key',
|
"Sec-WebSocket-Key",
|
||||||
'Sec-WebSocket-Version',
|
"Sec-WebSocket-Version",
|
||||||
'Sec-WebSocket-Extensions',
|
"Sec-WebSocket-Extensions",
|
||||||
'Sec-WebSocket-Protocol'
|
"Sec-WebSocket-Protocol",
|
||||||
],
|
],
|
||||||
maxAge: 86400,
|
maxAge: 86400,
|
||||||
credentials: true
|
credentials: true,
|
||||||
})
|
})
|
||||||
const router = AutoRouter<IRequest, [env: Environment, ctx: ExecutionContext]>({
|
const router = AutoRouter<IRequest, [env: Environment, ctx: ExecutionContext]>({
|
||||||
before: [preflight],
|
before: [preflight],
|
||||||
finally: [(response) => {
|
finally: [
|
||||||
// Add security headers to all responses except WebSocket upgrades
|
(response) => {
|
||||||
if (response.status !== 101) {
|
// Add security headers to all responses except WebSocket upgrades
|
||||||
Object.entries(securityHeaders).forEach(([key, value]) => {
|
if (response.status !== 101) {
|
||||||
response.headers.set(key, value)
|
Object.entries(securityHeaders).forEach(([key, value]) => {
|
||||||
})
|
response.headers.set(key, value)
|
||||||
}
|
})
|
||||||
return corsify(response)
|
}
|
||||||
}],
|
return corsify(response)
|
||||||
catch: (e) => {
|
},
|
||||||
console.error(e)
|
],
|
||||||
return error(e)
|
catch: (e) => {
|
||||||
},
|
console.error(e)
|
||||||
|
return error(e)
|
||||||
|
},
|
||||||
})
|
})
|
||||||
// requests to /connect are routed to the Durable Object, and handle realtime websocket syncing
|
// requests to /connect are routed to the Durable Object, and handle realtime websocket syncing
|
||||||
.get('/connect/:roomId', (request, env) => {
|
.get("/connect/:roomId", (request, env) => {
|
||||||
const id = env.TLDRAW_DURABLE_OBJECT.idFromName(request.params.roomId)
|
const id = env.TLDRAW_DURABLE_OBJECT.idFromName(request.params.roomId)
|
||||||
const room = env.TLDRAW_DURABLE_OBJECT.get(id)
|
const room = env.TLDRAW_DURABLE_OBJECT.get(id)
|
||||||
return room.fetch(request.url, {
|
return room.fetch(request.url, {
|
||||||
headers: request.headers,
|
headers: request.headers,
|
||||||
body: request.body,
|
body: request.body,
|
||||||
method: request.method
|
method: request.method,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// assets can be uploaded to the bucket under /uploads:
|
// assets can be uploaded to the bucket under /uploads:
|
||||||
.post('/uploads/:uploadId', handleAssetUpload)
|
.post("/uploads/:uploadId", handleAssetUpload)
|
||||||
|
|
||||||
// they can be retrieved from the bucket too:
|
// they can be retrieved from the bucket too:
|
||||||
.get('/uploads/:uploadId', handleAssetDownload)
|
.get("/uploads/:uploadId", handleAssetDownload)
|
||||||
|
|
||||||
// bookmarks need to extract metadata from pasted URLs:
|
// bookmarks need to extract metadata from pasted URLs:
|
||||||
.get('/unfurl', handleUnfurlRequest)
|
.get("/unfurl", handleUnfurlRequest)
|
||||||
|
|
||||||
.get('/room/:roomId', (request, env) => {
|
.get("/room/:roomId", (request, env) => {
|
||||||
const id = env.TLDRAW_DURABLE_OBJECT.idFromName(request.params.roomId)
|
const id = env.TLDRAW_DURABLE_OBJECT.idFromName(request.params.roomId)
|
||||||
const room = env.TLDRAW_DURABLE_OBJECT.get(id)
|
const room = env.TLDRAW_DURABLE_OBJECT.get(id)
|
||||||
return room.fetch(request.url, {
|
return room.fetch(request.url, {
|
||||||
headers: request.headers,
|
headers: request.headers,
|
||||||
body: request.body,
|
body: request.body,
|
||||||
method: request.method
|
method: request.method,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
.post('/room/:roomId', async (request, env) => {
|
.post("/room/:roomId", async (request, env) => {
|
||||||
const id = env.TLDRAW_DURABLE_OBJECT.idFromName(request.params.roomId)
|
const id = env.TLDRAW_DURABLE_OBJECT.idFromName(request.params.roomId)
|
||||||
const room = env.TLDRAW_DURABLE_OBJECT.get(id)
|
const room = env.TLDRAW_DURABLE_OBJECT.get(id)
|
||||||
return room.fetch(request.url, {
|
return room.fetch(request.url, {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
body: request.body
|
body: request.body,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
.post('/daily/rooms', async (request, env) => {
|
.post("/daily/rooms", async (request, env) => {
|
||||||
const response = await fetch('https://api.daily.co/v1/rooms', {
|
try {
|
||||||
method: 'POST',
|
// Log the request for debugging
|
||||||
headers: {
|
console.log(
|
||||||
'Authorization': `Bearer ${env.DAILY_API_KEY}`,
|
"Creating Daily room with API key:",
|
||||||
'Content-Type': 'application/json'
|
env.DAILY_API_KEY ? "present" : "missing",
|
||||||
},
|
)
|
||||||
body: await request.text()
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json() as Record<string, unknown>;
|
const response = await fetch("https://api.daily.co/v1/rooms", {
|
||||||
return new Response(JSON.stringify({
|
method: "POST",
|
||||||
...data,
|
headers: {
|
||||||
url: `https://${env.DAILY_DOMAIN}/${data.name}`
|
Authorization: `Bearer ${env.DAILY_API_KEY}`,
|
||||||
}), {
|
"Content-Type": "application/json",
|
||||||
headers: { 'Content-Type': 'application/json' }
|
},
|
||||||
});
|
body: await request.text(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorText = await response.text()
|
||||||
|
console.error("Daily API error:", errorText)
|
||||||
|
return new Response(`Daily API error: ${errorText}`, {
|
||||||
|
status: response.status,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
// Log successful response
|
||||||
|
console.log("Daily room created:", data)
|
||||||
|
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
...(data as Record<string, unknown>),
|
||||||
|
url: `https://${env.DAILY_DOMAIN}/${
|
||||||
|
(data as Record<string, unknown>).name
|
||||||
|
}`,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
// Add CORS headers specifically for this endpoint if needed
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
||||||
|
"Access-Control-Allow-Headers": "Content-Type",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error creating Daily room:", error)
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ error: "Failed to create Daily room" }),
|
||||||
|
{
|
||||||
|
status: 500,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// export our router for cloudflare
|
// export our router for cloudflare
|
||||||
export default router
|
export default router
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue