Files
ladill-meet/app/Http/Controllers/Meet/MeetingRoomController.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

413 lines
16 KiB
PHP

<?php
namespace App\Http\Controllers\Meet;
use App\Http\Controllers\Controller;
use App\Models\Participant;
use App\Models\Recording;
use App\Models\Room;
use App\Models\Session;
use App\Services\Meet\ChatService;
use App\Services\Meet\Media\MediaProviderInterface;
use App\Services\Meet\ParticipantPresenter;
use App\Services\Meet\RecordingService;
use App\Services\Meet\SecurityPolicyService;
use App\Services\Meet\SessionService;
use App\Services\Meet\SpaceService;
use App\Services\Meet\StageLayoutService;
use App\Services\Meet\TranscriptService;
use App\Services\Meet\WebinarService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class MeetingRoomController extends Controller
{
public function __construct(
protected SessionService $sessions,
protected ChatService $chat,
protected MediaProviderInterface $media,
protected RecordingService $recordings,
protected TranscriptService $transcripts,
protected SecurityPolicyService $security,
) {}
public function show(Request $request, Session $session): View|RedirectResponse
{
if (! $session->isLive()) {
return redirect()->route('meet.ended', $session);
}
$room = $session->room;
$kind = $room->isConference() ? 'conference' : 'meeting';
$participantUuid = $request->session()->get("meet.participant.{$session->uuid}");
abort_unless($participantUuid, 403, "Please join the {$kind} first.");
$participant = Participant::where('uuid', $participantUuid)
->where('session_id', $session->id)
->firstOrFail();
if ($participant->status === 'waiting') {
return redirect()->route('meet.join.waiting', $session->room);
}
abort_unless($participant->status === 'joined', 403, "You are not in this {$kind}.");
$token = $this->media->isConfigured()
? $this->sessions->generateMediaToken($participant)
: '';
$session->load(['room', 'participants']);
$room = $session->room;
$webinars = app(WebinarService::class);
$spaces = app(SpaceService::class);
$isAudioOnly = $room->isAudioOnly();
$stageLayout = app(StageLayoutService::class);
$authUser = $request->user();
$isRoomHost = $authUser && $authUser->ownerRef() === $room->host_user_ref;
$canManageConference = $participant->isHost() || $isRoomHost;
return view('meet.room.show', [
'session' => $session,
'room' => $room,
'participant' => $participant,
'mediaToken' => $token,
'mediaUrl' => $this->media->serverUrl(),
'mediaConfigured' => $this->media->isConfigured(),
'messages' => $this->chat->recentMessages($session),
'activeRecording' => $session->recordings()->where('status', 'recording')->first(),
'watermark' => $room->organization?->securitySetting('watermark_enabled', false),
'isWebinar' => $room->isWebinar() || $room->isConference() || $room->isSpace(),
'isConference' => $room->isConference(),
'isGreenRoom' => $session->session_mode === 'green_room',
'sessionMode' => $session->session_mode,
'presenterRefs' => (array) $session->presenter_refs,
'isAudioOnly' => $isAudioOnly,
'isSpace' => $room->isSpace(),
'canPublish' => $room->isSpace()
? $spaces->canPublish($room, $participant)
: $webinars->canPublish($room, $participant),
'isAttendee' => $participant->isAttendee() || $spaces->isListener($room, $participant),
'usesStageLayout' => $stageLayout->usesStageLayout($room),
'stageConfig' => $stageLayout->config($room),
'canManageConference' => $canManageConference,
]);
}
public function end(Request $request, Session $session): RedirectResponse
{
$participantUuid = $request->session()->get("meet.participant.{$session->uuid}");
$participant = Participant::where('uuid', $participantUuid)->first();
abort_unless($participant?->isHost(), 403);
$this->sessions->end($session, $participant->user_ref ?? $participant->uuid);
$room = $session->room;
return redirect()->route($this->roomShowRoute($room), $room)
->with('success', $this->roomEndedMessage($room));
}
protected function roomShowRoute(Room $room): string
{
if ($room->isWebinar()) {
return 'meet.webinars.show';
}
if ($room->isConference()) {
return 'meet.conferences.show';
}
if ($room->isSpace()) {
return 'meet.spaces.show';
}
return 'meet.rooms.show';
}
protected function roomEndedMessage(Room $room): string
{
if ($room->isWebinar()) {
return 'Webinar ended.';
}
if ($room->isConference()) {
return 'Conference ended.';
}
if ($room->isSpace()) {
return 'Room closed.';
}
return 'Meeting ended.';
}
public function leave(Request $request, Session $session): RedirectResponse
{
$participantUuid = $request->session()->get("meet.participant.{$session->uuid}");
$participant = Participant::where('uuid', $participantUuid)->first();
if ($participant) {
$this->sessions->leaveParticipant($participant);
$request->session()->put('meet.left.feedback', [
'session_uuid' => $session->uuid,
'participant_uuid' => $participant->uuid,
'display_name' => $participant->display_name,
'user_ref' => $participant->user_ref,
]);
}
$request->session()->forget("meet.participant.{$session->uuid}");
return redirect()->route('meet.left', $session);
}
public function raiseHand(Request $request, Session $session): JsonResponse
{
$participant = $this->currentParticipant($request, $session);
$participant->update(['hand_raised' => ! $participant->hand_raised]);
return response()->json(['hand_raised' => $participant->hand_raised]);
}
public function sendMessage(Request $request, Session $session): JsonResponse
{
$participant = $this->currentParticipant($request, $session);
$validated = $request->validate(['body' => ['required', 'string', 'max:2000']]);
$message = $this->chat->sendPublicMessage(
$session,
$participant->display_name,
$validated['body'],
$participant->user_ref,
);
return response()->json([
'message' => [
'uuid' => $message->uuid,
'sender_name' => $message->sender_name,
'body' => $message->body,
'created_at' => $message->created_at->toIso8601String(),
],
]);
}
public function sendReaction(Request $request, Session $session): JsonResponse
{
$participant = $this->currentParticipant($request, $session);
$validated = $request->validate(['emoji' => ['required', 'string', 'max:32']]);
$reaction = $this->chat->sendReaction(
$session,
$participant->display_name,
$validated['emoji'],
$participant->uuid,
);
return response()->json([
'reaction' => [
'sender_name' => $reaction->sender_name,
'emoji' => $reaction->emoji,
'created_at' => $reaction->created_at->toIso8601String(),
],
]);
}
public function poll(Request $request, Session $session): JsonResponse
{
if (! $session->isLive()) {
return response()->json([
'ended' => true,
'redirect' => route('meet.ended', $session),
]);
}
$this->currentParticipant($request, $session);
$session->load(['participants' => fn ($q) => $q->whereIn('status', ['joined', 'waiting'])]);
$avatars = ParticipantPresenter::avatarMap($session->participants, $session->room->host_user_ref);
$messages = $this->chat->recentMessages($session, 50);
$reactions = $session->reactions()->where('created_at', '>=', now()->subMinutes(5))->latest()->limit(20)->get();
$qa = app(\App\Services\Meet\QaService::class)->forSession($session, true);
$polls = $session->polls()->where('status', 'open')->with('votes')->get();
$breakouts = $session->breakoutRooms()->where('status', 'open')->get();
$broadcasts = $session->messages()->where('type', 'breakout_broadcast')->where('created_at', '>=', now()->subMinutes(30))->latest()->limit(5)->get();
$files = $session->sessionFiles()
->where(function ($q) {
$q->whereNull('expires_at')->orWhere('expires_at', '>', now());
})
->latest()
->limit(20)
->get();
return response()->json(array_merge([
'participants' => $session->participants->map(
fn ($p) => ParticipantPresenter::toArray($p, $avatars, $session->room->host_user_ref)
),
'waiting_count' => $session->participants->where('status', 'waiting')->count(),
'messages' => $messages->map(fn ($m) => [
'uuid' => $m->uuid,
'sender_name' => $m->sender_name,
'body' => $m->body,
'type' => $m->type,
'created_at' => $m->created_at->toIso8601String(),
]),
'reactions' => $reactions->map(fn ($r) => [
'sender_name' => $r->sender_name,
'emoji' => $r->emoji,
'created_at' => $r->created_at->toIso8601String(),
]),
'qa' => $qa->map(fn ($q) => [
'uuid' => $q->uuid,
'asker_name' => $q->asker_name,
'question' => $q->question,
'status' => $q->status,
'answer' => $q->answer,
'upvotes' => $q->upvotes,
]),
'polls' => $polls->map(fn ($p) => [
'uuid' => $p->uuid,
'question' => $p->question,
'options' => $p->options,
'votes' => $p->votes->count(),
]),
'breakouts' => $breakouts->map(fn ($b) => [
'uuid' => $b->uuid,
'name' => $b->name,
'closes_at' => $b->closes_at?->toIso8601String(),
]),
'broadcasts' => $broadcasts->map(fn ($m) => [
'body' => $m->body,
'sender_name' => $m->sender_name,
'created_at' => $m->created_at->toIso8601String(),
]),
'files' => $files->map(fn ($f) => [
'uuid' => $f->uuid,
'original_name' => $f->original_name,
'file_size' => $f->file_size,
]),
], app(StageLayoutService::class)->config($session->room)));
}
public function startRecording(Request $request, Session $session): JsonResponse
{
$participant = $this->currentParticipant($request, $session);
abort_unless($participant->isHost(), 403);
abort_if($session->room->organization?->securitySetting('recording_host_only') && ! $participant->isHost(), 403);
$host = $request->user() ?? \App\Models\User::where('public_id', $session->room->host_user_ref)->first();
abort_unless($host, 403);
$recording = $this->recordings->start($session, $host);
return response()->json(['recording_uuid' => $recording->uuid, 'status' => $recording->status]);
}
public function stopRecording(Request $request, Session $session): JsonResponse
{
$participant = $this->currentParticipant($request, $session);
abort_unless($participant->isHost(), 403);
$recording = $session->recordings()->where('status', 'recording')->firstOrFail();
$recording = $this->recordings->stop($recording, $participant->user_ref ?? $participant->uuid);
return response()->json([
'status' => 'processing',
'recording_uuid' => $recording->uuid,
]);
}
public function uploadRecording(Request $request, Session $session, Recording $recording): JsonResponse
{
$participant = $this->currentParticipant($request, $session);
abort_unless($participant->isHost(), 403);
abort_unless($recording->session_id === $session->id, 404);
$maxKb = max(1, (int) config('meet.recordings.max_mb', 512)) * 1024;
$request->validate([
'recording' => ['required', 'file', 'mimetypes:video/webm,video/mp4,video/x-matroska,video/quicktime', 'max:'.$maxKb],
]);
$this->recordings->storeUpload($recording, $request->file('recording'));
return response()->json(['status' => 'ready', 'recording_uuid' => $recording->uuid]);
}
public function lock(Request $request, Session $session): JsonResponse
{
$participant = $this->currentParticipant($request, $session);
abort_unless($participant->isHost(), 403);
$this->sessions->lock($session, $participant->user_ref ?? $participant->uuid);
return response()->json(['locked' => true]);
}
public function unlock(Request $request, Session $session): JsonResponse
{
$participant = $this->currentParticipant($request, $session);
abort_unless($participant->isHost(), 403);
$this->sessions->unlock($session, $participant->user_ref ?? $participant->uuid);
return response()->json(['locked' => false]);
}
public function caption(Request $request, Session $session): JsonResponse
{
$participant = $this->currentParticipant($request, $session);
$validated = $request->validate(['text' => ['required', 'string', 'max:500']]);
if (! $session->room->setting('live_captions', false)) {
return response()->json(['ok' => false], 422);
}
$transcript = $session->transcripts()->latest()->first()
?? $this->transcripts->ensureForSession($session);
$this->transcripts->appendSegment($transcript, $participant->display_name, $validated['text']);
return response()->json(['ok' => true]);
}
public function admit(Request $request, Session $session, Participant $participant): JsonResponse
{
$current = $this->currentParticipant($request, $session);
abort_unless($current->isHost(), 403);
abort_unless($participant->session_id === $session->id, 404);
$this->sessions->admitParticipant($participant);
return response()->json(['ok' => true, 'status' => 'joined']);
}
public function deny(Request $request, Session $session, Participant $participant): JsonResponse
{
$current = $this->currentParticipant($request, $session);
abort_unless($current->isHost(), 403);
abort_unless($participant->session_id === $session->id, 404);
$this->sessions->denyParticipant($participant);
return response()->json(['ok' => true, 'status' => 'removed']);
}
protected function currentParticipant(Request $request, Session $session): Participant
{
$participantUuid = $request->session()->get("meet.participant.{$session->uuid}");
abort_unless($participantUuid, 403);
return Participant::where('uuid', $participantUuid)
->where('session_id', $session->id)
->firstOrFail();
}
}