diff --git a/database/migrations/functions/common/get_event_full.sql b/database/migrations/functions/common/get_event_full.sql index aaff0eae..d8e52588 100644 --- a/database/migrations/functions/common/get_event_full.sql +++ b/database/migrations/functions/common/get_event_full.sql @@ -61,6 +61,7 @@ returns json as $$ 'meeting_join_url', coalesce(m_event.join_url, e.meeting_join_url), 'meeting_password', m_event.password, 'meeting_provider', e.meeting_provider_id, + 'meeting_recording_requested', e.meeting_recording_requested, 'meeting_recording_url', coalesce(e.meeting_recording_url, m_event.recording_url), 'meeting_requested', e.meeting_requested, 'meetup_url', e.meetup_url, diff --git a/database/migrations/functions/dashboard-group/add_event.sql b/database/migrations/functions/dashboard-group/add_event.sql index d6acee63..bf50a27a 100644 --- a/database/migrations/functions/dashboard-group/add_event.sql +++ b/database/migrations/functions/dashboard-group/add_event.sql @@ -120,6 +120,7 @@ begin meeting_join_instructions, meeting_join_url, meeting_provider_id, + meeting_recording_requested, meeting_recording_url, meeting_requested, meetup_url, @@ -171,6 +172,7 @@ begin nullif(p_event->>'meeting_join_instructions', ''), nullif(p_event->>'meeting_join_url', ''), nullif(p_event->>'meeting_provider_id', ''), + coalesce((p_event->>'meeting_recording_requested')::boolean, true), nullif(p_event->>'meeting_recording_url', ''), (p_event->>'meeting_requested')::boolean, nullif(p_event->>'meetup_url', ''), diff --git a/database/migrations/functions/dashboard-group/is_event_meeting_in_sync.sql b/database/migrations/functions/dashboard-group/is_event_meeting_in_sync.sql index 6c255377..882c9576 100644 --- a/database/migrations/functions/dashboard-group/is_event_meeting_in_sync.sql +++ b/database/migrations/functions/dashboard-group/is_event_meeting_in_sync.sql @@ -9,6 +9,7 @@ declare v_before_host_ids uuid[]; v_before_meeting_hosts text[] := case when p_before_event->'meeting_hosts' is not null then array(select jsonb_array_elements_text(p_before_event->'meeting_hosts')) else null end; v_before_meeting_provider_id text := p_before_event->>'meeting_provider_id'; + v_before_meeting_recording_requested boolean := coalesce((p_before_event->>'meeting_recording_requested')::boolean, true); v_before_meeting_requested boolean := coalesce((p_before_event->>'meeting_requested')::boolean, false); v_before_name text := p_before_event->>'name'; v_before_speaker_ids uuid[]; @@ -19,6 +20,7 @@ declare v_after_host_ids uuid[]; v_after_meeting_hosts text[] := case when p_after_event->'meeting_hosts' is not null then array(select jsonb_array_elements_text(p_after_event->'meeting_hosts')) else null end; v_after_meeting_provider_id text := p_after_event->>'meeting_provider_id'; + v_after_meeting_recording_requested boolean := coalesce((p_after_event->>'meeting_recording_requested')::boolean, true); v_after_meeting_requested boolean := (p_after_event->>'meeting_requested')::boolean; v_after_name text := p_after_event->>'name'; v_after_speaker_ids uuid[]; @@ -63,6 +65,7 @@ begin and v_before_host_ids is not distinct from v_after_host_ids and v_before_meeting_hosts is not distinct from v_after_meeting_hosts and v_before_meeting_provider_id is not distinct from v_after_meeting_provider_id + and v_before_meeting_recording_requested = v_after_meeting_recording_requested and v_before_name = v_after_name and v_before_speaker_ids is not distinct from v_after_speaker_ids and v_before_starts_at is not distinct from v_after_starts_at diff --git a/database/migrations/functions/dashboard-group/is_session_meeting_in_sync.sql b/database/migrations/functions/dashboard-group/is_session_meeting_in_sync.sql index 43709812..f71c9938 100644 --- a/database/migrations/functions/dashboard-group/is_session_meeting_in_sync.sql +++ b/database/migrations/functions/dashboard-group/is_session_meeting_in_sync.sql @@ -9,6 +9,7 @@ returns boolean as $$ declare v_after_ends_at timestamptz; v_after_event_host_ids uuid[]; + v_after_event_meeting_recording_requested boolean := coalesce((p_after_event->>'meeting_recording_requested')::boolean, true); v_after_meeting_hosts text[] := case when p_after_session->'meeting_hosts' is not null then array(select jsonb_array_elements_text(p_after_session->'meeting_hosts')) else null end; v_after_meeting_provider_id text := p_after_session->>'meeting_provider_id'; v_after_meeting_requested boolean := (p_after_session->>'meeting_requested')::boolean; @@ -20,6 +21,7 @@ declare v_before_ends_at timestamptz := to_timestamp((p_before_session->>'ends_at')::double precision); v_before_event_host_ids uuid[]; + v_before_event_meeting_recording_requested boolean := coalesce((p_before_event->>'meeting_recording_requested')::boolean, true); v_before_meeting_hosts text[] := case when p_before_session->'meeting_hosts' is not null then array(select jsonb_array_elements_text(p_before_session->'meeting_hosts')) else null end; v_before_meeting_provider_id text := p_before_session->>'meeting_provider_id'; v_before_meeting_requested boolean := coalesce((p_before_session->>'meeting_requested')::boolean, false); @@ -70,6 +72,7 @@ begin v_in_sync := v_before_meeting_requested = true and v_before_ends_at is not distinct from v_after_ends_at and v_before_event_host_ids is not distinct from v_after_event_host_ids + and v_before_event_meeting_recording_requested = v_after_event_meeting_recording_requested and v_before_meeting_hosts is not distinct from v_after_meeting_hosts and v_before_meeting_provider_id is not distinct from v_after_meeting_provider_id and v_before_name = v_after_name diff --git a/database/migrations/functions/dashboard-group/update_event.sql b/database/migrations/functions/dashboard-group/update_event.sql index d2634824..06593fe4 100644 --- a/database/migrations/functions/dashboard-group/update_event.sql +++ b/database/migrations/functions/dashboard-group/update_event.sql @@ -246,6 +246,7 @@ begin meeting_join_instructions = nullif(p_event->>'meeting_join_instructions', ''), meeting_join_url = nullif(p_event->>'meeting_join_url', ''), meeting_provider_id = p_event->>'meeting_provider_id', + meeting_recording_requested = coalesce((p_event->>'meeting_recording_requested')::boolean, true), meeting_recording_url = nullif(p_event->>'meeting_recording_url', ''), meeting_requested = (p_event->>'meeting_requested')::boolean, meetup_url = nullif(p_event->>'meetup_url', ''), diff --git a/database/migrations/functions/meetings/claim_meeting_out_of_sync.sql b/database/migrations/functions/meetings/claim_meeting_out_of_sync.sql index 5f842761..3ca3cbd4 100644 --- a/database/migrations/functions/meetings/claim_meeting_out_of_sync.sql +++ b/database/migrations/functions/meetings/claim_meeting_out_of_sync.sql @@ -51,6 +51,7 @@ begin 'join_url', m.join_url, 'meeting_id', m.meeting_id, 'meeting_provider_id', e.meeting_provider_id, + 'meeting_recording_requested', e.meeting_recording_requested, 'password', m.password, 'provider_host_user_id', e.meeting_provider_host_user, 'provider_meeting_id', m.provider_meeting_id, @@ -112,6 +113,7 @@ begin 'join_url', m.join_url, 'meeting_id', m.meeting_id, 'meeting_provider_id', s.meeting_provider_id, + 'meeting_recording_requested', e.meeting_recording_requested, 'password', m.password, 'provider_host_user_id', s.meeting_provider_host_user, 'provider_meeting_id', m.provider_meeting_id, diff --git a/database/migrations/functions/meetings/get_event_meeting_sync_state_hash.sql b/database/migrations/functions/meetings/get_event_meeting_sync_state_hash.sql index 9e5239df..202b0635 100644 --- a/database/migrations/functions/meetings/get_event_meeting_sync_state_hash.sql +++ b/database/migrations/functions/meetings/get_event_meeting_sync_state_hash.sql @@ -24,6 +24,7 @@ create or replace function get_event_meeting_sync_state_hash( from unnest(e.meeting_hosts) as meeting_host(email) ), 'meeting_provider_id', e.meeting_provider_id, + 'meeting_recording_requested', e.meeting_recording_requested, 'meeting_requested', e.meeting_requested, 'name', e.name, 'published', e.published, diff --git a/database/migrations/functions/meetings/get_session_meeting_sync_state_hash.sql b/database/migrations/functions/meetings/get_session_meeting_sync_state_hash.sql index da6ba115..0253c815 100644 --- a/database/migrations/functions/meetings/get_session_meeting_sync_state_hash.sql +++ b/database/migrations/functions/meetings/get_session_meeting_sync_state_hash.sql @@ -13,6 +13,7 @@ create or replace function get_session_meeting_sync_state_hash( where eh.event_id = e.event_id ), 'event_published', e.published, + 'event_meeting_recording_requested', e.meeting_recording_requested, 'event_timezone', e.timezone, 'ends_at', s.ends_at, 'meeting_hosts', ( diff --git a/database/migrations/schema/0038_add_event_meeting_recording_requested.sql b/database/migrations/schema/0038_add_event_meeting_recording_requested.sql new file mode 100644 index 00000000..d15c05e2 --- /dev/null +++ b/database/migrations/schema/0038_add_event_meeting_recording_requested.sql @@ -0,0 +1,4 @@ +-- Add organizer control for automatic event meeting recordings. + +alter table event + add column meeting_recording_requested boolean default true not null; diff --git a/database/tests/functions/common/get_event_full.sql b/database/tests/functions/common/get_event_full.sql index 62fa429b..66b9a78f 100644 --- a/database/tests/functions/common/get_event_full.sql +++ b/database/tests/functions/common/get_event_full.sql @@ -820,6 +820,7 @@ select is( "meeting_password": "event-secret", "meeting_requested": false, "meeting_join_url": "https://meeting.example.com/event", + "meeting_recording_requested": true, "meeting_recording_url": "https://meeting.example.com/event-recording", "meetup_url": "https://meetup.com/event123", "photos_urls": ["https://example.com/photo1.jpg", "https://example.com/photo2.jpg"], diff --git a/database/tests/functions/dashboard-group/add_event.sql b/database/tests/functions/dashboard-group/add_event.sql index 611d2e9f..62dbcde9 100644 --- a/database/tests/functions/dashboard-group/add_event.sql +++ b/database/tests/functions/dashboard-group/add_event.sql @@ -3,7 +3,7 @@ -- ============================================================================ begin; -select plan(32); +select plan(33); -- ============================================================================ -- VARIABLES @@ -114,6 +114,7 @@ select ok( "sessions": {}, "timezone": "America/New_York", "attendee_approval_required": false, + "meeting_recording_requested": true, "waitlist_count": 0, "waitlist_enabled": false }'::jsonb, @@ -274,6 +275,7 @@ select ok( "meeting_hosts": ["host1@example.com", "host2@example.com"], "meeting_join_instructions": "Use the waiting room display name from your ticket.", "meeting_join_url": "https://youtube.com/live", + "meeting_recording_requested": true, "meeting_recording_url": "https://youtube.com/recording", "meetup_url": "https://meetup.com/event", "photos_urls": ["https://example.com/photo1.jpg", "https://example.com/photo2.jpg"], @@ -467,6 +469,7 @@ select is( select jsonb_build_object( 'event', jsonb_build_object( 'meeting_hosts', meeting_hosts, + 'meeting_recording_requested', meeting_recording_requested, 'meeting_requested', meeting_requested, 'meeting_in_sync', meeting_in_sync ), @@ -486,6 +489,7 @@ select is( '{ "event": { "meeting_hosts": ["event-alt-host@example.com"], + "meeting_recording_requested": true, "meeting_requested": true, "meeting_in_sync": false }, @@ -498,6 +502,31 @@ select is( 'Should set meeting flags and hosts for event and session when requested' ); +-- Should persist explicit event meeting recording preference +select add_event( + null::uuid, + :'groupID'::uuid, + '{ + "name": "Meeting Recording Disabled Event", + "description": "Event requesting meeting support without recording", + "timezone": "UTC", + "category_id": "00000000-0000-0000-0000-000000000011", + "kind_id": "virtual", + "capacity": 100, + "starts_at": "2030-03-02T10:00:00", + "ends_at": "2030-03-02T11:30:00", + "meeting_provider_id": "zoom", + "meeting_recording_requested": false, + "meeting_requested": true + }'::jsonb +) as "recordingDisabledEventID" \gset + +select is( + (select meeting_recording_requested from event where event_id = :'recordingDisabledEventID'::uuid), + false, + 'Should persist event meeting recording preference when disabled' +); + -- Should throw error when capacity exceeds max_participants with meeting_requested select throws_ok( $$select add_event( diff --git a/database/tests/functions/dashboard-group/is_event_meeting_in_sync.sql b/database/tests/functions/dashboard-group/is_event_meeting_in_sync.sql index 3840f383..c8449f42 100644 --- a/database/tests/functions/dashboard-group/is_event_meeting_in_sync.sql +++ b/database/tests/functions/dashboard-group/is_event_meeting_in_sync.sql @@ -3,7 +3,7 @@ -- ============================================================================ begin; -select plan(14); +select plan(15); -- ============================================================================ -- TESTS @@ -176,6 +176,32 @@ select is( 'Event timezone change desyncs meeting' ); +-- Recording preference change desyncs meeting +select is( + is_event_meeting_in_sync( + '{ + "name": "Sync Event", + "timezone": "America/New_York", + "kind": "virtual", + "starts_at": 1748786400, + "ends_at": 1748790000, + "meeting_recording_requested": true, + "meeting_requested": true + }'::jsonb, + '{ + "name": "Sync Event", + "timezone": "America/New_York", + "kind_id": "virtual", + "starts_at": "2025-06-01T10:00:00", + "ends_at": "2025-06-01T11:00:00", + "meeting_recording_requested": false, + "meeting_requested": true + }'::jsonb + ), + false, + 'Event recording preference change desyncs meeting' +); + -- meeting_hosts unchanged keeps sync select is( is_event_meeting_in_sync( diff --git a/database/tests/functions/dashboard-group/is_session_meeting_in_sync.sql b/database/tests/functions/dashboard-group/is_session_meeting_in_sync.sql index 93a91fa4..2c22a8d1 100644 --- a/database/tests/functions/dashboard-group/is_session_meeting_in_sync.sql +++ b/database/tests/functions/dashboard-group/is_session_meeting_in_sync.sql @@ -3,7 +3,7 @@ -- ============================================================================ begin; -select plan(16); +select plan(17); -- ============================================================================ -- TESTS @@ -176,6 +176,36 @@ select is( 'Event timezone change desyncs session meeting' ); +-- Parent event recording preference change desyncs session meeting +select is( + is_session_meeting_in_sync( + '{ + "name": "Session One", + "session_kind_id": "virtual", + "starts_at": 1748787300, + "ends_at": 1748789100, + "meeting_requested": true + }'::jsonb, + '{ + "name": "Session One", + "kind": "virtual", + "starts_at": "2025-06-01T10:15:00", + "ends_at": "2025-06-01T10:45:00", + "meeting_requested": true + }'::jsonb, + '{ + "timezone": "America/New_York", + "meeting_recording_requested": true + }'::jsonb, + '{ + "timezone": "America/New_York", + "meeting_recording_requested": false + }'::jsonb + ), + false, + 'Parent event recording preference change desyncs session meeting' +); + -- Kind change from hybrid to in-person desyncs meeting select is( is_session_meeting_in_sync( diff --git a/database/tests/functions/dashboard-group/update_event.sql b/database/tests/functions/dashboard-group/update_event.sql index 555720c7..2cc7daa0 100644 --- a/database/tests/functions/dashboard-group/update_event.sql +++ b/database/tests/functions/dashboard-group/update_event.sql @@ -3,7 +3,7 @@ -- ============================================================================ begin; -select plan(100); +select plan(101); -- ============================================================================ -- VARIABLES @@ -1119,6 +1119,7 @@ select is( "has_ticket_purchases": false, "meeting_in_sync": false, "meeting_provider": "zoom", + "meeting_recording_requested": true, "meeting_requested": true, "sessions": {}, "starts_at": 1896213600, @@ -1735,6 +1736,7 @@ select is( "logo_url": "https://example.com/new-logo.png", "meeting_join_instructions": "Use the event ticket name when joining.", "meeting_join_url": "https://youtube.com/new-live", + "meeting_recording_requested": true, "meeting_recording_url": "https://youtube.com/new-recording", "meetup_url": "https://meetup.com/new-event", "photos_urls": ["https://example.com/new-photo1.jpg", "https://example.com/new-photo2.jpg"], @@ -2007,6 +2009,7 @@ select lives_ok( "capacity": 100, "kind_id": "virtual", "meeting_provider_id": "zoom", + "meeting_recording_requested": false, "meeting_recording_url": "https://youtube.com/watch?v=event-override", "meeting_requested": true, "starts_at": "2030-03-01T10:00:00", @@ -2020,6 +2023,11 @@ select is( 'https://youtube.com/watch?v=event-override', 'Should persist event recording override for automatic meetings' ); +select is( + (select meeting_recording_requested from event where event_id = :'event5ID'::uuid), + false, + 'Should persist event meeting recording preference when disabled' +); -- Should clear event recording override and fall back to synced meeting recording select lives_ok( diff --git a/database/tests/functions/meetings/claim_meeting_out_of_sync.sql b/database/tests/functions/meetings/claim_meeting_out_of_sync.sql index b8eec9a8..b1f1a884 100644 --- a/database/tests/functions/meetings/claim_meeting_out_of_sync.sql +++ b/database/tests/functions/meetings/claim_meeting_out_of_sync.sql @@ -653,6 +653,7 @@ select is( "event_id": "00000000-0000-0000-0000-000000001512", "hosts": ["eventhost@example.com", "eventspeaker@example.com", "explicit@example.com"], "meeting_provider_id": "zoom", + "meeting_recording_requested": true, "timezone": "UTC", "topic": "Event Create Test" }'::jsonb, @@ -677,6 +678,7 @@ select is( "join_url": "https://zoom.us/j/event-update", "meeting_id": "00000000-0000-0000-0000-000000001533", "meeting_provider_id": "zoom", + "meeting_recording_requested": true, "password": "eventpass", "provider_meeting_id": "event-update", "timezone": "UTC", @@ -698,6 +700,7 @@ select is( "duration_secs": 1800, "hosts": ["eventhost@example.com", "sessionhost@example.com", "sessionspeaker@example.com"], "meeting_provider_id": "zoom", + "meeting_recording_requested": true, "session_id": "00000000-0000-0000-0000-000000001523", "timezone": "UTC", "topic": "Session Create Test" @@ -719,6 +722,7 @@ select is( "join_url": "https://zoom.us/j/session-update", "meeting_id": "00000000-0000-0000-0000-000000001534", "meeting_provider_id": "zoom", + "meeting_recording_requested": true, "password": "sessionpass", "provider_meeting_id": "session-update", "session_id": "00000000-0000-0000-0000-000000001524", @@ -1075,6 +1079,7 @@ select is( "duration_secs": 3600, "event_id": "00000000-0000-0000-0000-000000001517", "meeting_provider_id": "zoom", + "meeting_recording_requested": true, "timezone": "UTC", "topic": "Event Unpublished Test" }'::jsonb, diff --git a/database/tests/functions/meetings/get_event_meeting_sync_state_hash.sql b/database/tests/functions/meetings/get_event_meeting_sync_state_hash.sql index 14d8fb45..4a28282e 100644 --- a/database/tests/functions/meetings/get_event_meeting_sync_state_hash.sql +++ b/database/tests/functions/meetings/get_event_meeting_sync_state_hash.sql @@ -3,7 +3,7 @@ -- ============================================================================ begin; -select plan(5); +select plan(6); -- ============================================================================ -- VARIABLES @@ -116,6 +116,15 @@ where event_id = :'eventID'; insert into event_sync_hash (label, sync_state_hash) values ('after_meeting_hosts_change', get_event_meeting_sync_state_hash(:'eventID')); +-- Capture hashes before and after changing the recording preference +insert into event_sync_hash (label, sync_state_hash) +values ('before_recording_requested_change', get_event_meeting_sync_state_hash(:'eventID')); +update event +set meeting_recording_requested = false +where event_id = :'eventID'; +insert into event_sync_hash (label, sync_state_hash) +values ('after_recording_requested_change', get_event_meeting_sync_state_hash(:'eventID')); + -- Capture hashes before and after changing event hosts insert into event_sync_hash (label, sync_state_hash) values ('before_event_hosts_change', get_event_meeting_sync_state_hash(:'eventID')); @@ -165,6 +174,16 @@ select is( 'Should change hash when explicit meeting hosts change' ); +-- Changing recording preference should change the hash +select is( + (select before.sync_state_hash <> after.sync_state_hash + from event_sync_hash before, event_sync_hash after + where before.label = 'before_recording_requested_change' + and after.label = 'after_recording_requested_change'), + true, + 'Should change hash when event meeting recording preference changes' +); + -- Changing event hosts should change the hash select is( (select before.sync_state_hash <> after.sync_state_hash diff --git a/database/tests/functions/meetings/get_session_meeting_sync_state_hash.sql b/database/tests/functions/meetings/get_session_meeting_sync_state_hash.sql index 611b9dfc..6aeabd29 100644 --- a/database/tests/functions/meetings/get_session_meeting_sync_state_hash.sql +++ b/database/tests/functions/meetings/get_session_meeting_sync_state_hash.sql @@ -3,7 +3,7 @@ -- ============================================================================ begin; -select plan(5); +select plan(6); -- ============================================================================ -- VARIABLES @@ -140,6 +140,15 @@ where event_id = :'eventID'; insert into session_sync_hash (label, sync_state_hash) values ('after_parent_event_change', get_session_meeting_sync_state_hash(:'sessionID')); +-- Capture hashes before and after changing the parent recording preference +insert into session_sync_hash (label, sync_state_hash) +values ('before_parent_recording_requested_change', get_session_meeting_sync_state_hash(:'sessionID')); +update event +set meeting_recording_requested = false +where event_id = :'eventID'; +insert into session_sync_hash (label, sync_state_hash) +values ('after_parent_recording_requested_change', get_session_meeting_sync_state_hash(:'sessionID')); + -- Capture hashes before and after changing session speakers insert into session_sync_hash (label, sync_state_hash) values ('before_session_speakers_change', get_session_meeting_sync_state_hash(:'sessionID')); @@ -189,6 +198,16 @@ select is( 'Should change hash when parent event meeting input changes' ); +-- Changing parent recording preference should change the hash +select is( + (select before.sync_state_hash <> after.sync_state_hash + from session_sync_hash before, session_sync_hash after + where before.label = 'before_parent_recording_requested_change' + and after.label = 'after_parent_recording_requested_change'), + true, + 'Should change hash when parent event meeting recording preference changes' +); + -- Changing session speakers should change the hash select is( (select before.sync_state_hash <> after.sync_state_hash diff --git a/database/tests/schema/02_columns.sql b/database/tests/schema/02_columns.sql index 444bdd7b..d933e339 100644 --- a/database/tests/schema/02_columns.sql +++ b/database/tests/schema/02_columns.sql @@ -224,6 +224,7 @@ select columns_are('event', array[ 'meeting_join_url', 'meeting_provider_host_user', 'meeting_provider_id', + 'meeting_recording_requested', 'meeting_recording_url', 'meeting_requested', 'meeting_sync_claimed_at', diff --git a/ocg-server/src/services/meetings.rs b/ocg-server/src/services/meetings.rs index 9ed85d26..ef5e01b6 100644 --- a/ocg-server/src/services/meetings.rs +++ b/ocg-server/src/services/meetings.rs @@ -593,6 +593,8 @@ pub(crate) struct Meeting { pub password: Option, pub provider_host_user_id: Option, pub provider_meeting_id: Option, + #[serde(alias = "meeting_recording_requested")] + pub recording_requested: Option, pub session_id: Option, pub starts_at: Option>, #[serde(skip_serializing)] diff --git a/ocg-server/src/services/meetings/zoom/client.rs b/ocg-server/src/services/meetings/zoom/client.rs index ffb477fb..899ae38d 100644 --- a/ocg-server/src/services/meetings/zoom/client.rs +++ b/ocg-server/src/services/meetings/zoom/client.rs @@ -316,7 +316,7 @@ impl TryFrom<&Meeting> for CreateMeetingRequest { default_password: Some(true), duration: m.duration.map(Minutes::try_from_duration).transpose()?, - settings: Some(default_meeting_settings()), + settings: Some(default_meeting_settings(m.recording_requested)), start_time: m.starts_at, timezone: m.timezone.clone(), }) @@ -375,7 +375,7 @@ impl TryFrom<&Meeting> for UpdateMeetingRequest { fn try_from(m: &Meeting) -> Result { Ok(Self { duration: m.duration.map(Minutes::try_from_duration).transpose()?, - settings: Some(default_meeting_settings()), + settings: Some(default_meeting_settings(m.recording_requested)), start_time: m.starts_at, timezone: m.timezone.clone(), topic: m.topic.clone(), @@ -502,9 +502,13 @@ pub(crate) struct ZoomMeeting { } /// Returns the default settings applied to all meetings. -fn default_meeting_settings() -> MeetingSettings { +fn default_meeting_settings(recording_requested: Option) -> MeetingSettings { MeetingSettings { - auto_recording: Some("cloud".to_string()), + auto_recording: Some(if recording_requested.unwrap_or(true) { + "cloud".to_string() + } else { + "none".to_string() + }), jbh_time: Some(15), join_before_host: Some(true), mute_upon_entry: Some(true), @@ -512,3 +516,96 @@ fn default_meeting_settings() -> MeetingSettings { waiting_room: Some(false), } } + +#[cfg(test)] +mod tests { + use serde_json::json; + + use crate::services::meetings::Meeting; + + use super::{CreateMeetingRequest, UpdateMeetingRequest}; + + #[test] + fn create_meeting_request_sets_cloud_recording_when_requested() { + let request = CreateMeetingRequest::try_from(&Meeting { + recording_requested: Some(true), + ..Default::default() + }) + .unwrap(); + + assert_eq!( + serde_json::to_value(request).unwrap()["settings"]["auto_recording"], + json!("cloud") + ); + } + + #[test] + fn create_meeting_request_sets_cloud_recording_by_default() { + let request = CreateMeetingRequest::try_from(&Meeting { + recording_requested: None, + ..Default::default() + }) + .unwrap(); + + assert_eq!( + serde_json::to_value(request).unwrap()["settings"]["auto_recording"], + json!("cloud") + ); + } + + #[test] + fn create_meeting_request_sets_no_recording_when_not_requested() { + let request = CreateMeetingRequest::try_from(&Meeting { + recording_requested: Some(false), + ..Default::default() + }) + .unwrap(); + + assert_eq!( + serde_json::to_value(request).unwrap()["settings"]["auto_recording"], + json!("none") + ); + } + + #[test] + fn update_meeting_request_sets_cloud_recording_when_requested() { + let request = UpdateMeetingRequest::try_from(&Meeting { + recording_requested: Some(true), + ..Default::default() + }) + .unwrap(); + + assert_eq!( + serde_json::to_value(request).unwrap()["settings"]["auto_recording"], + json!("cloud") + ); + } + + #[test] + fn update_meeting_request_sets_cloud_recording_by_default() { + let request = UpdateMeetingRequest::try_from(&Meeting { + recording_requested: None, + ..Default::default() + }) + .unwrap(); + + assert_eq!( + serde_json::to_value(request).unwrap()["settings"]["auto_recording"], + json!("cloud") + ); + } + + #[test] + fn update_meeting_request_sets_no_recording_when_not_requested() { + let request = UpdateMeetingRequest::try_from(&Meeting { + recording_requested: Some(false), + ..Default::default() + }) + .unwrap(); + + assert_eq!( + serde_json::to_value(request).unwrap()["settings"]["auto_recording"], + json!("none") + ); + } +} diff --git a/ocg-server/src/templates/dashboard/group/events.rs b/ocg-server/src/templates/dashboard/group/events.rs index f3c0c0c3..1bd3829b 100644 --- a/ocg-server/src/templates/dashboard/group/events.rs +++ b/ocg-server/src/templates/dashboard/group/events.rs @@ -296,6 +296,9 @@ pub(crate) struct Event { #[serde(rename = "meeting_provider_id")] #[garde(skip)] pub meeting_provider: Option, + /// Whether automatic event meetings should be recorded. + #[garde(skip)] + pub meeting_recording_requested: Option, /// Recording URL for meeting. #[garde(url, length(max = MAX_LEN_L))] pub meeting_recording_url: Option, diff --git a/ocg-server/src/types/event.rs b/ocg-server/src/types/event.rs index 77adfbd7..b1aeffa5 100644 --- a/ocg-server/src/types/event.rs +++ b/ocg-server/src/types/event.rs @@ -286,6 +286,8 @@ pub struct EventFull { pub meeting_password: Option, /// Desired meeting provider for this event. pub meeting_provider: Option, + /// Whether automatic event meetings should be recorded. + pub meeting_recording_requested: Option, /// URL for meeting recording. pub meeting_recording_url: Option, /// Whether the event requests a meeting. diff --git a/ocg-server/static/js/common/online-event-details.js b/ocg-server/static/js/common/online-event-details.js index af6d46ac..c47ab158 100644 --- a/ocg-server/static/js/common/online-event-details.js +++ b/ocg-server/static/js/common/online-event-details.js @@ -33,6 +33,13 @@ export class OnlineEventDetails extends LitWrapper { }, meetingJoinUrl: { type: String, attribute: "meeting-join-url" }, meetingRecordingUrl: { type: String, attribute: "meeting-recording-url" }, + meetingRecordingRequested: { + type: Boolean, + attribute: "meeting-recording-requested", + converter: { + fromAttribute: (value) => value !== "false", + }, + }, meetingRequested: { type: Boolean, attribute: "meeting-requested" }, meetingHosts: { type: Array, @@ -74,6 +81,7 @@ export class OnlineEventDetails extends LitWrapper { _joinInstructions: { type: String, state: true }, _joinUrl: { type: String, state: true }, _recordingUrl: { type: String, state: true }, + _recordingRequested: { type: Boolean, state: true }, _createMeeting: { type: Boolean, state: true }, _providerId: { type: String, state: true }, _hosts: { type: Array, state: true }, @@ -87,6 +95,7 @@ export class OnlineEventDetails extends LitWrapper { this.meetingJoinInstructions = ""; this.meetingJoinUrl = ""; this.meetingRecordingUrl = ""; + this.meetingRecordingRequested = true; this.meetingRequested = false; this.meetingHosts = []; this.startsAt = ""; @@ -102,6 +111,7 @@ export class OnlineEventDetails extends LitWrapper { this._joinInstructions = ""; this._joinUrl = ""; this._recordingUrl = ""; + this._recordingRequested = true; this._createMeeting = false; this._providerId = DEFAULT_MEETING_PROVIDER; this._hosts = []; @@ -135,6 +145,7 @@ export class OnlineEventDetails extends LitWrapper { this._joinInstructions = this.meetingJoinInstructions || ""; this._joinUrl = this._manualJoinUrl; this._recordingUrl = startsInAutomaticMode ? this._automaticRecordingUrl : this._manualRecordingUrl; + this._recordingRequested = this.meetingRecordingRequested !== false; this._createMeeting = this.meetingRequested; this._providerId = this.meetingProviderId || DEFAULT_MEETING_PROVIDER; this._hosts = Array.isArray(this.meetingHosts) ? [...this.meetingHosts] : []; @@ -589,6 +600,15 @@ export class OnlineEventDetails extends LitWrapper { this._manualRecordingUrl = this._recordingUrl; } + /** + * Handles the automatic recording request toggle. + * @param {Event} e - Change event + */ + _handleRecordingRequestedChange(e) { + if (this.disabled) return; + this._recordingRequested = e.target.checked; + } + _getAutomaticAvailability() { if (this.disabled) { return { allowed: false, reasons: [] }; @@ -664,6 +684,7 @@ export class OnlineEventDetails extends LitWrapper { const isAutomatic = this._mode === "automatic" && this._createMeeting; const data = { meeting_join_url: isAutomatic ? "" : (this._joinUrl || "").trim(), + meeting_recording_requested: this._recordingRequested !== false, meeting_recording_url: (this._recordingUrl || "").trim(), meeting_requested: isAutomatic, meeting_provider_id: isAutomatic ? (this._providerId || DEFAULT_MEETING_PROVIDER).trim() : "", @@ -684,6 +705,7 @@ export class OnlineEventDetails extends LitWrapper { this._joinInstructions = ""; this._joinUrl = ""; this._recordingUrl = ""; + this._recordingRequested = true; this._createMeeting = false; this._providerId = DEFAULT_MEETING_PROVIDER; this._hosts = []; @@ -717,6 +739,7 @@ export class OnlineEventDetails extends LitWrapper { const { meeting_join_instructions: joinInstructionsValue, meeting_join_url: joinUrlValue, + meeting_recording_requested: recordingRequestedValue, meeting_recording_url: recordingUrlValue, meeting_requested: isAutomatic, meeting_provider_id: providerIdValue, @@ -733,6 +756,11 @@ export class OnlineEventDetails extends LitWrapper { /> ` : ""} + + ${!this._isSession() + ? html` +
+ +

+ When enabled, Zoom will automatically record this meeting to the cloud. +

+
+ ` + : ""} ` : ""} diff --git a/ocg-server/templates/dashboard/group/events_update.html b/ocg-server/templates/dashboard/group/events_update.html index dd3ddd6c..078913ab 100644 --- a/ocg-server/templates/dashboard/group/events_update.html +++ b/ocg-server/templates/dashboard/group/events_update.html @@ -698,6 +698,10 @@ meeting-recording-url="{{ meeting_recording_url }}" {% endif %} + {% if let Some(meeting_recording_requested) = &event.meeting_recording_requested %} + meeting-recording-requested="{{ meeting_recording_requested }}" + {% endif %} + {% if event.meeting_requested.unwrap_or(false) %}meeting-requested="true"{% endif %} {% if let Some(meeting_in_sync) = &event.meeting_in_sync %} diff --git a/tests/unit/common/online-event-details.test.js b/tests/unit/common/online-event-details.test.js index e10af127..77a75d10 100644 --- a/tests/unit/common/online-event-details.test.js +++ b/tests/unit/common/online-event-details.test.js @@ -1,7 +1,11 @@ import { expect } from "@open-wc/testing"; import "/static/js/common/online-event-details.js"; -import { mountLitComponent, useMountedElementsCleanup } from "/tests/unit/test-utils/lit.js"; +import { + mountLitComponent, + mountLitComponentWithAttributes, + useMountedElementsCleanup, +} from "/tests/unit/test-utils/lit.js"; describe("online-event-details", () => { useMountedElementsCleanup("online-event-details"); @@ -16,6 +20,7 @@ describe("online-event-details", () => { expect(element.getMeetingData()).to.deep.equal({ meeting_join_instructions: "Bring your ticket confirmation.", meeting_join_url: "https://example.com/join", + meeting_recording_requested: true, meeting_recording_url: "https://example.com/recording", meeting_requested: false, meeting_provider_id: "", @@ -29,6 +34,21 @@ describe("online-event-details", () => { expect(element._recordingUrl).to.equal(""); }); + it("honors a server-rendered false recording request attribute", async () => { + const element = await mountLitComponentWithAttributes( + "online-event-details", + { + attributes: { + "meeting-recording-requested": "false", + }, + }, + ); + + expect(element.getMeetingData()).to.include({ + meeting_recording_requested: false, + }); + }); + it("shows a capacity warning when automatic meeting capacity is exceeded", async () => { const capacity = document.createElement("input"); capacity.id = "capacity"; @@ -70,6 +90,7 @@ describe("online-event-details", () => { expect(element.getMeetingData()).to.deep.equal({ meeting_join_instructions: "", meeting_join_url: "", + meeting_recording_requested: true, meeting_recording_url: "", meeting_requested: false, meeting_provider_id: "", @@ -106,6 +127,7 @@ describe("online-event-details", () => { expect(element.getMeetingData()).to.deep.equal({ meeting_join_instructions: "", meeting_join_url: "", + meeting_recording_requested: true, meeting_recording_url: "https://zoom.us/rec/share/synced", meeting_requested: true, meeting_provider_id: "zoom", @@ -132,10 +154,13 @@ describe("online-event-details", () => { }); expect(element._mode).to.equal("manual"); - expect(element._recordingUrl).to.equal(" https://youtube.com/watch?v=processed "); + expect(element._recordingUrl).to.equal( + " https://youtube.com/watch?v=processed ", + ); expect(element.getMeetingData()).to.deep.equal({ meeting_join_instructions: "", meeting_join_url: "", + meeting_recording_requested: true, meeting_recording_url: "https://youtube.com/watch?v=processed", meeting_requested: false, meeting_provider_id: "", @@ -163,10 +188,13 @@ describe("online-event-details", () => { expect(element._mode).to.equal("automatic"); expect(element._joinUrl).to.equal(""); - expect(element._recordingUrl).to.equal(" https://youtube.com/watch?v=processed "); + expect(element._recordingUrl).to.equal( + " https://youtube.com/watch?v=processed ", + ); expect(element.getMeetingData()).to.deep.equal({ meeting_join_instructions: "", meeting_join_url: "", + meeting_recording_requested: true, meeting_recording_url: "https://youtube.com/watch?v=processed", meeting_requested: true, meeting_provider_id: "zoom", @@ -189,6 +217,7 @@ describe("online-event-details", () => { expect(element.getMeetingData()).to.deep.equal({ meeting_join_instructions: "", meeting_join_url: "", + meeting_recording_requested: true, meeting_recording_url: "https://youtube.com/watch?v=session-processed", meeting_requested: true, meeting_provider_id: "zoom",