participant($request, $session); $validated = $request->validate(['question' => ['required', 'string', 'max:1000']]); $question = $this->qa->submit($session, $participant, $validated['question']); return response()->json(['question' => $question]); } public function approveQuestion(Request $request, Session $session, QaQuestion $question): JsonResponse { $this->hostOnly($request, $session); abort_unless($question->session_id === $session->id, 404); return response()->json(['question' => $this->qa->approve($question)]); } public function answerQuestion(Request $request, Session $session, QaQuestion $question): JsonResponse { $participant = $this->hostOnly($request, $session); abort_unless($question->session_id === $session->id, 404); $validated = $request->validate(['answer' => ['required', 'string', 'max:2000']]); return response()->json([ 'question' => $this->qa->answer($question, $validated['answer'], $participant->display_name), ]); } public function upvoteQuestion(Request $request, Session $session, QaQuestion $question): JsonResponse { $this->participant($request, $session); abort_unless($question->session_id === $session->id, 404); return response()->json(['question' => $this->qa->upvote($question)]); } // --- Polls --- public function createPoll(Request $request, Session $session): JsonResponse { $this->hostOnly($request, $session); $validated = $request->validate([ 'question' => ['required', 'string', 'max:500'], 'options' => ['required', 'array', 'min:2', 'max:6'], 'options.*' => ['required', 'string', 'max:200'], ]); $poll = $this->polls->create($session, $validated['question'], $validated['options']); return response()->json(['poll' => $poll], 201); } public function votePoll(Request $request, Session $session, MeetPoll $poll): JsonResponse { $participant = $this->participant($request, $session); abort_unless($poll->session_id === $session->id, 404); $validated = $request->validate(['option_index' => ['required', 'integer', 'min:0']]); $this->polls->vote($poll, $participant, $validated['option_index']); return response()->json($this->polls->results($poll)); } public function closePoll(Request $request, Session $session, MeetPoll $poll): JsonResponse { $this->hostOnly($request, $session); abort_unless($poll->session_id === $session->id, 404); return response()->json($this->polls->results($this->polls->close($poll))); } // --- Breakouts --- public function createBreakouts(Request $request, Session $session): JsonResponse { $this->hostOnly($request, $session); $validated = $request->validate([ 'count' => ['required', 'integer', 'min:1', 'max:20'], 'timer_minutes' => ['nullable', 'integer', 'min:5', 'max:120'], ]); $rooms = $this->breakouts->createRooms( $session, $validated['count'], $validated['timer_minutes'] ?? null, ); return response()->json(['breakouts' => $rooms]); } public function moveToBreakout(Request $request, Session $session, BreakoutRoom $breakout): JsonResponse { $participant = $this->participant($request, $session); abort_unless($breakout->session_id === $session->id, 404); $this->breakouts->moveParticipant($participant, $breakout); return response()->json(['breakout_uuid' => $breakout->uuid, 'media_room' => $breakout->media_room_name]); } public function returnFromBreakout(Request $request, Session $session): JsonResponse { $participant = $this->participant($request, $session); $this->breakouts->returnToMain($participant); return response()->json(['returned' => true]); } public function broadcastBreakout(Request $request, Session $session): JsonResponse { $participant = $this->hostOnly($request, $session); $validated = $request->validate(['message' => ['required', 'string', 'max:1000']]); $message = $this->breakouts->broadcast($session, $validated['message'], $participant->display_name); return response()->json(['message' => $message]); } public function closeBreakouts(Request $request, Session $session): JsonResponse { $this->hostOnly($request, $session); $this->breakouts->closeAll($session); return response()->json(['closed' => true]); } // --- Files --- public function uploadFile(Request $request, Session $session): JsonResponse { $participant = $this->participant($request, $session); $validated = $request->validate(['file' => ['required', 'file', 'max:51200']]); $file = $this->files->upload( $session->room, $session, $validated['file'], $participant->user_ref ?? $participant->uuid, $participant->display_name, ); return response()->json(['file' => $file], 201); } public function downloadFile(Request $request, Session $session, SessionFile $file): StreamedResponse|JsonResponse { $participant = $this->participant($request, $session); abort_unless($file->room_id === $session->room_id, 404); if ($file->drive_file_id) { return response()->json(['url' => $this->files->download($file, $participant)]); } $path = $this->files->download($file, $participant); return Storage::disk(config('meet.files.disk', 'local'))->download($file->storage_path, $file->original_name); } // --- Whiteboard --- public function showWhiteboard(Request $request, Session $session): JsonResponse { $this->participant($request, $session); $board = $this->whiteboards->ensureForSession($session); return response()->json(['whiteboard' => $board]); } public function saveWhiteboard(Request $request, Session $session): JsonResponse { $this->participant($request, $session); $validated = $request->validate(['state' => ['required', 'array']]); $board = $this->whiteboards->ensureForSession($session); return response()->json(['whiteboard' => $this->whiteboards->saveState($board, $validated['state'])]); } public function exportWhiteboard(Request $request, Session $session): JsonResponse { $this->participant($request, $session); $validated = $request->validate(['image_data' => ['required', 'string']]); $data = preg_replace('#^data:image/\w+;base64,#i', '', $validated['image_data']); $binary = base64_decode($data, true); abort_unless($binary !== false, 422); $path = 'whiteboards/'.$session->uuid.'/'.now()->timestamp.'.png'; Storage::disk(config('meet.files.disk', 'local'))->put($path, $binary); return response()->json(['export_path' => $path]); } // --- Live streaming --- public function configureLiveStream(Request $request, Session $session): JsonResponse { $this->hostOnly($request, $session); $validated = $request->validate([ 'platform' => ['required', 'string', 'in:'.implode(',', config('meet.live_stream.platforms'))], 'rtmp_url' => ['nullable', 'url', 'max:2048'], 'stream_key' => ['required', 'string', 'max:512'], 'layout' => ['nullable', 'string', 'in:'.implode(',', config('meet.live_stream.layouts'))], ]); $stream = $this->liveStreams->configure($session, $request->user(), $validated); return response()->json(['stream' => $stream->makeHidden('stream_key')], 201); } public function startLiveStream(Request $request, Session $session): JsonResponse { $this->hostOnly($request, $session); $validated = $request->validate([ 'platform' => ['required', 'string', 'in:'.implode(',', config('meet.live_stream.platforms'))], ]); $stream = $session->liveStreams()->where('platform', $validated['platform'])->firstOrFail(); $started = $this->liveStreams->start($stream); return response()->json(['stream' => $started->makeHidden('stream_key')]); } public function stopLiveStream(Request $request, Session $session): JsonResponse { $this->hostOnly($request, $session); $validated = $request->validate([ 'platform' => ['required', 'string', 'in:'.implode(',', config('meet.live_stream.platforms'))], ]); $stream = $session->liveStreams()->where('platform', $validated['platform'])->firstOrFail(); return response()->json(['stream' => $this->liveStreams->stop($stream)->makeHidden('stream_key')]); } // --- Town hall --- public function startGreenRoom(Request $request, Session $session): JsonResponse { $this->hostOnly($request, $session); abort_unless($session->room->isTownHall(), 422); $greenRoom = $this->townHall->startGreenRoom($session->room, $request->user()); return response()->json(['session_uuid' => $greenRoom->uuid, 'mode' => 'green_room']); } public function goLiveTownHall(Request $request, Session $session): JsonResponse { $this->hostOnly($request, $session); $live = $this->townHall->goLive($session, $request->user()); $participant = app(SessionService::class)->joinedParticipantForUser($live, $request->user()); if ($participant) { app(SessionService::class)->rememberParticipantSession($request, $participant, $live); } return response()->json([ 'session_uuid' => $live->uuid, 'mode' => 'live', 'room_url' => route('meet.room', $live), ]); } public function addPresenter(Request $request, Session $session): JsonResponse { $this->hostOnly($request, $session); abort_unless($session->room->isTownHall(), 422); $validated = $request->validate(['user_ref' => ['required', 'string', 'max:64']]); $updated = $this->townHall->addPresenter($session, $validated['user_ref']); return response()->json([ 'session' => $updated, 'presenter_refs' => $updated->presenter_refs, ]); } public function handoffCoHost(Request $request, Session $session): JsonResponse { $participant = $this->hostOnly($request, $session); $validated = $request->validate(['user_ref' => ['required', 'string', 'max:64']]); return response()->json([ 'participant' => $this->townHall->handoffCoHost($session, $participant, $validated['user_ref']), ]); } public function updateStage(Request $request, Session $session): JsonResponse { $this->hostOnly($request, $session); $room = $session->room; abort_unless($this->stageLayout->usesStageLayout($room), 422); $validated = $request->validate([ 'panel_discussions' => ['sometimes', 'boolean'], 'stage_layout' => ['sometimes', 'string', 'in:plenary,panel'], 'active_panel_id' => ['nullable', 'string', 'max:64'], 'spotlight_speaker_ref' => ['nullable', 'string', 'max:64'], 'panels' => ['sometimes', 'array', 'max:12'], 'panels.*.id' => ['nullable', 'string', 'max:64'], 'panels.*.name' => ['required_with:panels', 'string', 'max:100'], 'panels.*.speaker_refs' => ['nullable', 'array'], 'panels.*.speaker_refs.*' => ['string', 'max:64'], ]); $room = $this->stageLayout->update($room, $validated); return response()->json($this->stageLayout->config($room)); } protected function participant(Request $request, Session $session): Participant { $uuid = $request->session()->get("meet.participant.{$session->uuid}"); abort_unless($uuid, 403); return Participant::where('uuid', $uuid)->where('session_id', $session->id)->firstOrFail(); } protected function hostOnly(Request $request, Session $session): Participant { $participant = $this->participant($request, $session); abort_unless($participant->isHost(), 403); return $participant; } }