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 */