validateRoomPayload($request); if (($validated['source']['app'] ?? '') === 'sales_centre') { $this->salesCentreWorkspace->ensureHost($validated); } $organization = $this->resolveOrganization($validated); abort_unless($organization->owner_ref === $validated['owner_ref'], 403); $roomUuid = trim((string) ($validated['meet_room_uuid'] ?? '')); if ($roomUuid !== '') { $existing = Room::query() ->where('uuid', $roomUuid) ->where('owner_ref', $validated['owner_ref']) ->first(); if ($existing) { return $this->respondWithRoom( $this->persistRoomUpdate($existing, $validated, $organization), ); } } $entityId = (string) ($validated['source']['entity_id'] ?? ''); if ($entityId !== '') { $existing = Room::query() ->forSource((string) $validated['source']['app'], $entityId, $validated['owner_ref']) ->first(); if ($existing) { return $this->respondWithRoom( $this->persistRoomUpdate($existing, $validated, $organization), ); } } return $this->respondWithRoom( $this->persistRoomCreate($validated, $organization), 201, ); } public function lookup(Request $request): JsonResponse { $validated = $request->validate([ 'source_app' => ['required', 'string'], 'entity_id' => ['required', 'string'], 'owner_ref' => ['nullable', 'string'], ]); $room = Room::query() ->forSource( $validated['source_app'], $validated['entity_id'], $validated['owner_ref'] ?? null, ) ->first(); if (! $room) { return response()->json(['error' => 'Room not found.'], 404); } return $this->respondWithRoom($room); } public function show(Room $room): JsonResponse { return $this->respondWithRoom($room->load('sessions')); } public function update(Request $request, Room $room): JsonResponse { $validated = $this->validateRoomPayload($request, partial: true); abort_unless($room->owner_ref === ($validated['owner_ref'] ?? $room->owner_ref), 403); $organization = isset($validated['organization_id']) ? Organization::findOrFail($validated['organization_id']) : $room->organization; return $this->respondWithRoom( $this->persistRoomUpdate($room, $validated, $organization), ); } public function cancel(Request $request, Room $room): JsonResponse { $validated = $request->validate([ 'owner_ref' => ['required', 'string'], 'host_user_ref' => ['nullable', 'string'], ]); abort_unless($room->owner_ref === $validated['owner_ref'], 403); $host = User::where('public_id', $validated['host_user_ref'] ?? $room->host_user_ref)->first(); $this->rooms->cancel($room, $validated['owner_ref'], $host); return $this->respondWithRoom($room->fresh()); } public function start(Request $request, Room $room): JsonResponse { $host = User::where('public_id', $request->input('host_user_ref', $room->host_user_ref))->firstOrFail(); abort_unless($room->host_user_ref === $host->ownerRef(), 403); $session = $this->sessions->start($room, $host); return response()->json([ 'session' => $session, 'room_url' => route('meet.room', $session), 'join_url' => $room->joinUrl(), ]); } /** @return array */ private function validateRoomPayload(Request $request, bool $partial = false): array { $required = $partial ? 'sometimes' : 'required'; return $request->validate([ 'owner_ref' => [$required, 'string'], 'organization_id' => ['nullable', 'integer', 'exists:meet_organizations,id'], 'host_user_ref' => [$partial ? 'sometimes' : 'required', 'string'], 'host_name' => ['nullable', 'string', 'max:255'], 'host_email' => ['nullable', 'email', 'max:255'], 'title' => [$partial ? 'sometimes' : 'required', 'string', 'max:255'], 'description' => ['nullable', 'string'], 'type' => ['nullable', 'string', Rule::in(self::SERVICE_TYPES)], 'scheduled_at' => ['nullable', 'date'], 'duration_minutes' => ['nullable', 'integer', 'min:15', 'max:480'], 'timezone' => ['nullable', 'timezone'], 'passcode' => ['nullable', 'string', 'max:20'], 'settings' => ['nullable', 'array'], 'source' => [$partial ? 'sometimes' : 'required', 'array'], 'source.app' => [$partial ? 'sometimes' : 'required', 'string'], 'source.entity_type' => ['nullable', 'string'], 'source.entity_id' => ['nullable', 'string'], 'source.event_id' => ['nullable', 'string'], 'invite_emails' => ['nullable', 'array'], 'invite_emails.*' => ['email'], 'meet_room_uuid' => ['nullable', 'string', 'max:64'], ]); } /** @param array $validated */ private function resolveOrganization(array $validated): Organization { if (isset($validated['organization_id'])) { return Organization::findOrFail($validated['organization_id']); } if (($validated['source']['app'] ?? '') === 'sales_centre') { $host = User::where('public_id', $validated['host_user_ref'])->firstOrFail(); return $this->salesCentreWorkspace->ensureOrganization($host); } return Organization::owned($validated['owner_ref'])->firstOrFail(); } /** @param array $validated */ private function persistRoomCreate(array $validated, Organization $organization): Room { $host = User::where('public_id', $validated['host_user_ref'])->firstOrFail(); $type = $this->resolveType($validated); $payload = array_merge($validated, [ 'type' => $type, 'status' => 'scheduled', 'settings' => $this->mergeSettingsForType($type, $validated['settings'] ?? []), ]); $room = $type === 'instant' && empty($validated['scheduled_at']) ? $this->rooms->createInstant($host, $organization, $payload) : $this->rooms->create($host, $organization, $payload); $this->afterRoomPersisted($room, $validated, $host); return $room->fresh(); } /** @param array $validated */ private function persistRoomUpdate(Room $room, array $validated, Organization $organization): Room { $hostRef = $validated['host_user_ref'] ?? $room->host_user_ref; $host = User::where('public_id', $hostRef)->firstOrFail(); $update = array_filter([ 'title' => $validated['title'] ?? null, 'description' => array_key_exists('description', $validated) ? $validated['description'] : null, 'scheduled_at' => $validated['scheduled_at'] ?? null, 'duration_minutes' => $validated['duration_minutes'] ?? null, 'timezone' => $validated['timezone'] ?? null, 'passcode' => array_key_exists('passcode', $validated) ? $validated['passcode'] : null, 'host_user_ref' => $hostRef, 'source' => $validated['source'] ?? null, ], fn ($value) => $value !== null); if (isset($validated['settings'])) { $type = $this->resolveType(array_merge(['type' => $room->type], $validated)); $update['settings'] = $this->mergeSettingsForType( $type, array_merge($room->settings ?? [], $validated['settings']), ); } $room = $this->rooms->update($room, $update, $host); $this->afterRoomPersisted($room, $validated, $host); return $room; } /** @param array $validated */ private function afterRoomPersisted(Room $room, array $validated, User $host): void { if (! empty($validated['invite_emails'])) { $invites = collect($validated['invite_emails'])->map(fn ($email) => ['email' => $email])->all(); $this->invitations->inviteToRoom($room, $invites, $host); } if ($room->scheduled_at) { $this->calendar->syncRoomCreated($room, $host); } } /** @param array $validated */ private function resolveType(array $validated): string { if (! empty($validated['type'])) { return (string) $validated['type']; } return empty($validated['scheduled_at']) ? 'instant' : 'scheduled'; } /** @param array $settings */ private function mergeSettingsForType(string $type, array $settings): array { $defaults = match ($type) { 'town_hall' => [ 'waiting_room' => true, 'mute_on_join' => true, 'green_room' => true, 'stage_layout' => 'plenary', 'stage_mode' => true, 'panel_discussions' => false, 'panels' => [], 'presenter_refs' => [], ], 'webinar' => [ 'waiting_room' => true, 'mute_on_join' => true, 'webinar_auto_approve' => false, 'stage_layout' => 'plenary', 'stage_mode' => true, 'panel_discussions' => false, 'panels' => [], ], default => [], }; return array_merge(config('meet.default_settings', []), $defaults, $settings); } private function respondWithRoom(Room $room, int $status = 200): JsonResponse { return response()->json([ 'room' => $room->loadMissing('sessions'), 'join_url' => $room->joinUrl(), 'embed_url' => route('meet.join', $room), ], $status); } }