diff --git a/server/migrations/versions/20251217000000_add_skip_consent_to_room_and_meeting.py b/server/migrations/versions/20251217000000_add_skip_consent_to_room_and_meeting.py
new file mode 100644
index 00000000..56230e4b
--- /dev/null
+++ b/server/migrations/versions/20251217000000_add_skip_consent_to_room_and_meeting.py
@@ -0,0 +1,48 @@
+"""add skip_consent to room and meeting
+
+Revision ID: 20251217000000
+Revises: bbafedfa510c
+Create Date: 2025-12-17 00:00:00.000000
+
+"""
+
+from typing import Sequence, Union
+
+import sqlalchemy as sa
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision: str = "20251217000000"
+down_revision: Union[str, None] = "bbafedfa510c"
+branch_labels: Union[str, Sequence[str], None] = None
+depends_on: Union[str, Sequence[str], None] = None
+
+
+def upgrade() -> None:
+ with op.batch_alter_table("room", schema=None) as batch_op:
+ batch_op.add_column(
+ sa.Column(
+ "skip_consent",
+ sa.Boolean(),
+ nullable=False,
+ server_default=sa.text("false"),
+ )
+ )
+
+ with op.batch_alter_table("meeting", schema=None) as batch_op:
+ batch_op.add_column(
+ sa.Column(
+ "skip_consent",
+ sa.Boolean(),
+ nullable=False,
+ server_default=sa.text("false"),
+ )
+ )
+
+
+def downgrade() -> None:
+ with op.batch_alter_table("meeting", schema=None) as batch_op:
+ batch_op.drop_column("skip_consent")
+
+ with op.batch_alter_table("room", schema=None) as batch_op:
+ batch_op.drop_column("skip_consent")
diff --git a/server/reflector/db/meetings.py b/server/reflector/db/meetings.py
index 8a80e756..1657d4a8 100644
--- a/server/reflector/db/meetings.py
+++ b/server/reflector/db/meetings.py
@@ -63,6 +63,12 @@ meetings = sa.Table(
nullable=False,
server_default=assert_equal(WHEREBY_PLATFORM, "whereby"),
),
+ sa.Column(
+ "skip_consent",
+ sa.Boolean,
+ nullable=False,
+ server_default=sa.false(),
+ ),
sa.Index("idx_meeting_room_id", "room_id"),
sa.Index("idx_meeting_calendar_event", "calendar_event_id"),
)
@@ -110,6 +116,7 @@ class Meeting(BaseModel):
calendar_event_id: str | None = None
calendar_metadata: dict[str, Any] | None = None
platform: Platform = WHEREBY_PLATFORM
+ skip_consent: bool = False
class MeetingController:
@@ -140,6 +147,7 @@ class MeetingController:
calendar_event_id=calendar_event_id,
calendar_metadata=calendar_metadata,
platform=room.platform,
+ skip_consent=room.skip_consent,
)
query = meetings.insert().values(**meeting.model_dump())
await get_database().execute(query)
diff --git a/server/reflector/db/rooms.py b/server/reflector/db/rooms.py
index fc6194e3..92ac5eac 100644
--- a/server/reflector/db/rooms.py
+++ b/server/reflector/db/rooms.py
@@ -57,6 +57,12 @@ rooms = sqlalchemy.Table(
sqlalchemy.String,
nullable=False,
),
+ sqlalchemy.Column(
+ "skip_consent",
+ sqlalchemy.Boolean,
+ nullable=False,
+ server_default=sqlalchemy.sql.false(),
+ ),
sqlalchemy.Index("idx_room_is_shared", "is_shared"),
sqlalchemy.Index("idx_room_ics_enabled", "ics_enabled"),
)
@@ -85,6 +91,7 @@ class Room(BaseModel):
ics_last_sync: datetime | None = None
ics_last_etag: str | None = None
platform: Platform = Field(default_factory=lambda: settings.DEFAULT_VIDEO_PLATFORM)
+ skip_consent: bool = False
class RoomController:
@@ -139,6 +146,7 @@ class RoomController:
ics_fetch_interval: int = 300,
ics_enabled: bool = False,
platform: Platform = settings.DEFAULT_VIDEO_PLATFORM,
+ skip_consent: bool = False,
):
"""
Add a new room
@@ -163,6 +171,7 @@ class RoomController:
"ics_fetch_interval": ics_fetch_interval,
"ics_enabled": ics_enabled,
"platform": platform,
+ "skip_consent": skip_consent,
}
room = Room(**room_data)
diff --git a/server/reflector/views/rooms.py b/server/reflector/views/rooms.py
index 5b218cb4..1a4413e9 100644
--- a/server/reflector/views/rooms.py
+++ b/server/reflector/views/rooms.py
@@ -44,6 +44,7 @@ class Room(BaseModel):
ics_last_sync: Optional[datetime] = None
ics_last_etag: Optional[str] = None
platform: Platform
+ skip_consent: bool = False
class RoomDetails(Room):
@@ -72,6 +73,7 @@ class Meeting(BaseModel):
calendar_event_id: str | None = None
calendar_metadata: dict[str, Any] | None = None
platform: Platform
+ skip_consent: bool = False
class CreateRoom(BaseModel):
@@ -90,6 +92,7 @@ class CreateRoom(BaseModel):
ics_fetch_interval: int = 300
ics_enabled: bool = False
platform: Platform
+ skip_consent: bool = False
class UpdateRoom(BaseModel):
@@ -108,6 +111,7 @@ class UpdateRoom(BaseModel):
ics_fetch_interval: Optional[int] = None
ics_enabled: Optional[bool] = None
platform: Optional[Platform] = None
+ skip_consent: Optional[bool] = None
class CreateRoomMeeting(BaseModel):
@@ -249,6 +253,7 @@ async def rooms_create(
ics_fetch_interval=room.ics_fetch_interval,
ics_enabled=room.ics_enabled,
platform=room.platform,
+ skip_consent=room.skip_consent,
)
diff --git a/www/app/(app)/rooms/page.tsx b/www/app/(app)/rooms/page.tsx
index 147f8351..f542e8e8 100644
--- a/www/app/(app)/rooms/page.tsx
+++ b/www/app/(app)/rooms/page.tsx
@@ -91,6 +91,7 @@ const roomInitialState = {
icsEnabled: false,
icsFetchInterval: 5,
platform: "whereby",
+ skipConsent: false,
};
export default function RoomsList() {
@@ -175,6 +176,7 @@ export default function RoomsList() {
icsEnabled: detailedEditedRoom.ics_enabled || false,
icsFetchInterval: detailedEditedRoom.ics_fetch_interval || 5,
platform: detailedEditedRoom.platform,
+ skipConsent: detailedEditedRoom.skip_consent || false,
}
: null,
[detailedEditedRoom],
@@ -326,6 +328,7 @@ export default function RoomsList() {
ics_enabled: room.icsEnabled,
ics_fetch_interval: room.icsFetchInterval,
platform,
+ skip_consent: room.skipConsent,
};
if (isEditing) {
@@ -388,6 +391,7 @@ export default function RoomsList() {
icsEnabled: roomData.ics_enabled || false,
icsFetchInterval: roomData.ics_fetch_interval || 5,
platform: roomData.platform,
+ skipConsent: roomData.skip_consent || false,
});
setEditRoomId(roomId);
setIsEditing(true);
@@ -796,6 +800,34 @@ export default function RoomsList() {
Shared room
+ {room.recordingType === "cloud" && (
+
+ {
+ const syntheticEvent = {
+ target: {
+ name: "skipConsent",
+ type: "checkbox",
+ checked: e.checked,
+ },
+ };
+ handleRoomChange(syntheticEvent);
+ }}
+ >
+
+
+
+
+ Skip consent dialog
+
+
+ When enabled, participants won't be asked for
+ recording consent. Audio will be stored automatically.
+
+
+ )}
diff --git a/www/app/[roomName]/components/DailyRoom.tsx b/www/app/[roomName]/components/DailyRoom.tsx
index db051cc6..f626f314 100644
--- a/www/app/[roomName]/components/DailyRoom.tsx
+++ b/www/app/[roomName]/components/DailyRoom.tsx
@@ -8,6 +8,7 @@ import type { components } from "../../reflector-api";
import { useAuth } from "../../lib/AuthProvider";
import {
ConsentDialogButton,
+ RecordingIndicator,
recordingTypeRequiresConsent,
} from "../../lib/consent";
import { useRoomJoinMeeting } from "../../lib/apiHooks";
@@ -162,7 +163,12 @@ export default function DailyRoom({ meeting }: DailyRoomProps) {
{meeting.recording_type &&
recordingTypeRequiresConsent(meeting.recording_type) &&
- meeting.id && }
+ meeting.id &&
+ (meeting.skip_consent ? (
+
+ ) : (
+
+ ))}
);
}
diff --git a/www/app/[roomName]/components/WherebyRoom.tsx b/www/app/[roomName]/components/WherebyRoom.tsx
index d670b4e2..87e0f975 100644
--- a/www/app/[roomName]/components/WherebyRoom.tsx
+++ b/www/app/[roomName]/components/WherebyRoom.tsx
@@ -8,6 +8,7 @@ import { getWherebyUrl, useWhereby } from "../../lib/wherebyClient";
import { assertExistsAndNonEmptyString, NonEmptyString } from "../../lib/utils";
import {
ConsentDialogButton as BaseConsentDialogButton,
+ RecordingIndicator,
useConsentDialog,
recordingTypeRequiresConsent,
} from "../../lib/consent";
@@ -90,12 +91,15 @@ export default function WherebyRoom({ meeting }: WherebyRoomProps) {
/>
{recordingType &&
recordingTypeRequiresConsent(recordingType) &&
- meetingId && (
+ meetingId &&
+ (meeting.skip_consent ? (
+
+ ) : (
- )}
+ ))}
>
);
}
diff --git a/www/app/lib/consent/RecordingIndicator.tsx b/www/app/lib/consent/RecordingIndicator.tsx
new file mode 100644
index 00000000..882ef96d
--- /dev/null
+++ b/www/app/lib/consent/RecordingIndicator.tsx
@@ -0,0 +1,33 @@
+"use client";
+
+import { Box, Text } from "@chakra-ui/react";
+import { FaCircle } from "react-icons/fa6";
+import {
+ CONSENT_BUTTON_TOP_OFFSET,
+ CONSENT_BUTTON_LEFT_OFFSET,
+ CONSENT_BUTTON_Z_INDEX,
+} from "./constants";
+
+export function RecordingIndicator() {
+ return (
+
+
+ Recording
+
+ );
+}
diff --git a/www/app/lib/consent/index.ts b/www/app/lib/consent/index.ts
index eabca8ac..385db26d 100644
--- a/www/app/lib/consent/index.ts
+++ b/www/app/lib/consent/index.ts
@@ -2,6 +2,7 @@
export { ConsentDialogButton } from "./ConsentDialogButton";
export { ConsentDialog } from "./ConsentDialog";
+export { RecordingIndicator } from "./RecordingIndicator";
export { useConsentDialog } from "./useConsentDialog";
export { recordingTypeRequiresConsent } from "./utils";
export * from "./constants";
diff --git a/www/app/reflector-api.d.ts b/www/app/reflector-api.d.ts
index 4aa6ee36..069f1170 100644
--- a/www/app/reflector-api.d.ts
+++ b/www/app/reflector-api.d.ts
@@ -893,8 +893,16 @@ export interface components {
* @default false
*/
ics_enabled: boolean;
- /** Platform */
- platform?: ("whereby" | "daily") | null;
+ /**
+ * Platform
+ * @enum {string}
+ */
+ platform: "whereby" | "daily";
+ /**
+ * Skip Consent
+ * @default false
+ */
+ skip_consent: boolean;
};
/** CreateRoomMeeting */
CreateRoomMeeting: {
@@ -1123,7 +1131,9 @@ export interface components {
/** Audio Deleted */
audio_deleted?: boolean | null;
/** Participants */
- participants: components["schemas"]["TranscriptParticipant"][] | null;
+ participants:
+ | components["schemas"]["TranscriptParticipantWithEmail"][]
+ | null;
/**
* @description discriminator enum property added by openapi-typescript
* @enum {string}
@@ -1184,7 +1194,9 @@ export interface components {
/** Audio Deleted */
audio_deleted?: boolean | null;
/** Participants */
- participants: components["schemas"]["TranscriptParticipant"][] | null;
+ participants:
+ | components["schemas"]["TranscriptParticipantWithEmail"][]
+ | null;
};
/**
* GetTranscriptWithText
@@ -1246,7 +1258,9 @@ export interface components {
/** Audio Deleted */
audio_deleted?: boolean | null;
/** Participants */
- participants: components["schemas"]["TranscriptParticipant"][] | null;
+ participants:
+ | components["schemas"]["TranscriptParticipantWithEmail"][]
+ | null;
/**
* @description discriminator enum property added by openapi-typescript
* @enum {string}
@@ -1315,7 +1329,9 @@ export interface components {
/** Audio Deleted */
audio_deleted?: boolean | null;
/** Participants */
- participants: components["schemas"]["TranscriptParticipant"][] | null;
+ participants:
+ | components["schemas"]["TranscriptParticipantWithEmail"][]
+ | null;
/**
* @description discriminator enum property added by openapi-typescript
* @enum {string}
@@ -1386,7 +1402,9 @@ export interface components {
/** Audio Deleted */
audio_deleted?: boolean | null;
/** Participants */
- participants: components["schemas"]["TranscriptParticipant"][] | null;
+ participants:
+ | components["schemas"]["TranscriptParticipantWithEmail"][]
+ | null;
/**
* @description discriminator enum property added by openapi-typescript
* @enum {string}
@@ -1526,6 +1544,11 @@ export interface components {
* @enum {string}
*/
platform: "whereby" | "daily";
+ /**
+ * Skip Consent
+ * @default false
+ */
+ skip_consent: boolean;
};
/** MeetingConsentRequest */
MeetingConsentRequest: {
@@ -1567,6 +1590,11 @@ export interface components {
/** Name */
name: string;
};
+ /** ProcessStatus */
+ ProcessStatus: {
+ /** Status */
+ status: string;
+ };
/** Room */
Room: {
/** Id */
@@ -1617,6 +1645,11 @@ export interface components {
* @enum {string}
*/
platform: "whereby" | "daily";
+ /**
+ * Skip Consent
+ * @default false
+ */
+ skip_consent: boolean;
};
/** RoomDetails */
RoomDetails: {
@@ -1668,6 +1701,11 @@ export interface components {
* @enum {string}
*/
platform: "whereby" | "daily";
+ /**
+ * Skip Consent
+ * @default false
+ */
+ skip_consent: boolean;
/** Webhook Url */
webhook_url: string | null;
/** Webhook Secret */
@@ -1813,6 +1851,19 @@ export interface components {
/** User Id */
user_id?: string | null;
};
+ /** TranscriptParticipantWithEmail */
+ TranscriptParticipantWithEmail: {
+ /** Id */
+ id?: string;
+ /** Speaker */
+ speaker: number | null;
+ /** Name */
+ name: string;
+ /** User Id */
+ user_id?: string | null;
+ /** Email */
+ email?: string | null;
+ };
/**
* TranscriptSegment
* @description A single transcript segment with speaker and timing information.
@@ -1868,6 +1919,8 @@ export interface components {
ics_enabled?: boolean | null;
/** Platform */
platform?: ("whereby" | "daily") | null;
+ /** Skip Consent */
+ skip_consent?: boolean | null;
};
/** UpdateTranscript */
UpdateTranscript: {
@@ -3362,7 +3415,7 @@ export interface operations {
[name: string]: unknown;
};
content: {
- "application/json": unknown;
+ "application/json": components["schemas"]["ProcessStatus"];
};
};
/** @description Validation Error */