Files
ladill-meet/app/Http/Controllers/Meet/MeetingFeaturesController.php
T
isaaccladandCursor 965fb992e9
Deploy Ladill Meet / deploy (push) Failing after 7s
Initial Ladill Meet release.
Phases 0–18: core meetings, webinar, breakouts, team chat, live streaming, town hall, billing, and Ladill Mail calendar wiring.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-30 23:35:29 +00:00

315 lines
12 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\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,
) {}
// --- 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());
return response()->json(['session_uuid' => $live->uuid, 'mode' => 'live']);
}
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']),
]);
}
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;
}
}