Files
ladill-meet/app/Http/Controllers/Meet/MeetingFeaturesController.php
T
isaaccladandCursor 712dc487c6
Deploy Ladill Meet / deploy (push) Successful in 34s
Fix green room Go live and Panels hidden for conference hosts.
Stop registerPresenter from demoting hosts to panelists, recognize room owners as managers, and show stage controls in the green room banner.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-01 22:54:15 +00:00

373 lines
14 KiB
PHP

<?php
namespace App\Http\Controllers\Meet;
use App\Http\Controllers\Controller;
use App\Models\BreakoutRoom;
use App\Models\MeetPoll;
use App\Models\Participant;
use App\Models\QaQuestion;
use App\Models\Session;
use App\Models\SessionFile;
use App\Models\Whiteboard;
use App\Services\Meet\BreakoutService;
use App\Services\Meet\FileSharingService;
use App\Services\Meet\LiveStreamService;
use App\Services\Meet\PollService;
use App\Services\Meet\QaService;
use App\Services\Meet\StageLayoutService;
use App\Services\Meet\TownHallService;
use App\Services\Meet\WhiteboardService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\HttpFoundation\StreamedResponse;
class MeetingFeaturesController extends Controller
{
public function __construct(
protected QaService $qa,
protected PollService $polls,
protected BreakoutService $breakouts,
protected FileSharingService $files,
protected WhiteboardService $whiteboards,
protected LiveStreamService $liveStreams,
protected TownHallService $townHall,
protected StageLayoutService $stageLayout,
) {}
// --- Q&A ---
public function submitQuestion(Request $request, Session $session): JsonResponse
{
$participant = $this->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);
$user = $request->user();
$isRoomHost = $user && $user->ownerRef() === $session->room->host_user_ref;
abort_unless($participant->isHost() || $isRoomHost, 403);
if ($isRoomHost && ! $participant->isHost()) {
$participant->update(['role' => 'host']);
return $participant->fresh();
}
return $participant;
}
}