Deploy Ladill Meet / deploy (push) Failing after 7s
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>
287 lines
11 KiB
PHP
287 lines
11 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Meet;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Participant;
|
|
use App\Models\Session;
|
|
use App\Services\Meet\ChatService;
|
|
use App\Services\Meet\Media\MediaProviderInterface;
|
|
use App\Services\Meet\RecordingService;
|
|
use App\Services\Meet\SecurityPolicyService;
|
|
use App\Services\Meet\SessionService;
|
|
use App\Services\Meet\TranscriptService;
|
|
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
|
|
{
|
|
abort_unless($session->isLive(), 404, 'Meeting has ended.');
|
|
|
|
$participantUuid = $request->session()->get("meet.participant.{$session->uuid}");
|
|
abort_unless($participantUuid, 403, 'Please join the meeting first.');
|
|
|
|
$participant = Participant::where('uuid', $participantUuid)
|
|
->where('session_id', $session->id)
|
|
->firstOrFail();
|
|
|
|
$token = $this->media->isConfigured()
|
|
? $this->sessions->generateMediaToken($participant)
|
|
: '';
|
|
|
|
$session->load(['room', 'participants']);
|
|
|
|
return view('meet.room.show', [
|
|
'session' => $session,
|
|
'room' => $session->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' => $session->room->organization?->securitySetting('watermark_enabled', false),
|
|
'isWebinar' => $session->room->isWebinar(),
|
|
'canPublish' => app(\App\Services\Meet\WebinarService::class)->canPublish($session->room, $participant),
|
|
'isAttendee' => $participant->isAttendee(),
|
|
]);
|
|
}
|
|
|
|
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);
|
|
|
|
return redirect()->route('meet.rooms.show', $session->room)
|
|
->with('success', '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()->forget("meet.participant.{$session->uuid}");
|
|
|
|
return redirect()->route('meet.dashboard');
|
|
}
|
|
|
|
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
|
|
{
|
|
$this->currentParticipant($request, $session);
|
|
|
|
$session->load(['participants' => fn ($q) => $q->where('status', 'joined')]);
|
|
|
|
$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([
|
|
'participants' => $session->participants->map(fn ($p) => [
|
|
'uuid' => $p->uuid,
|
|
'display_name' => $p->display_name,
|
|
'role' => $p->role,
|
|
'hand_raised' => $p->hand_raised,
|
|
'is_muted' => $p->is_muted,
|
|
'is_video_off' => $p->is_video_off,
|
|
'breakout_room_id' => $p->breakout_room_id,
|
|
]),
|
|
'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,
|
|
]),
|
|
]);
|
|
}
|
|
|
|
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();
|
|
$this->recordings->stop($recording, $participant->user_ref ?? $participant->uuid);
|
|
|
|
return response()->json(['status' => 'processing']);
|
|
}
|
|
|
|
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]);
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|