Deploy Ladill Meet / deploy (push) Successful in 40s
Remove duplicate host record button on desktop and split invite-to-speak from make co-host so listeners must accept before speaking while co-host is applied immediately. Co-authored-by: Cursor <cursoragent@cursor.com>
685 lines
27 KiB
PHP
685 lines
27 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\BreakoutService;
|
|
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\TownHallService;
|
|
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}");
|
|
|
|
if (! $participantUuid && $request->user() && $request->user()->ownerRef() === $room->host_user_ref) {
|
|
$hostParticipant = $this->sessions->ensureJoinedParticipant($request, $session, $request->user(), 'host');
|
|
$participantUuid = $hostParticipant->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}.");
|
|
|
|
$participant = $this->sessions->syncEventsSpeakerRole($request, $participant);
|
|
|
|
$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;
|
|
$canManageSpeakAccess = app(\App\Services\Meet\SpeakAccessService::class)->canManageSpeakAccess($participant);
|
|
$usesSpeakAccess = $room->isWebinar() || $room->isConference();
|
|
$mediaState = $this->sessions->mediaSessionState($participant);
|
|
$breakoutService = app(BreakoutService::class);
|
|
$inBreakout = $breakoutService->isInActiveBreakout($participant);
|
|
|
|
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(),
|
|
'isConference' => $room->isConference(),
|
|
'isGreenRoom' => $session->session_mode === 'green_room',
|
|
'sessionMode' => $session->session_mode,
|
|
'presenterRefs' => (array) $session->presenter_refs,
|
|
'isAudioOnly' => $isAudioOnly,
|
|
'isSpace' => $room->isSpace(),
|
|
'canPublish' => $mediaState['can_publish'],
|
|
'audioOnlyPublish' => $mediaState['audio_only_publish'],
|
|
'speakGranted' => $mediaState['speak_granted'],
|
|
'speakRequested' => $mediaState['speak_requested'],
|
|
'usesSpeakAccess' => $usesSpeakAccess,
|
|
'canManageSpeakAccess' => $canManageSpeakAccess,
|
|
'isAttendee' => $participant->isAttendee() || $spaces->isListener($room, $participant),
|
|
'usesStageLayout' => $stageLayout->usesStageLayout($room) && ! $inBreakout,
|
|
'inBreakout' => $inBreakout,
|
|
'stageConfig' => $stageLayout->config($room),
|
|
'canManageConference' => $canManageConference,
|
|
'programmeSnapshot' => $room->setting('programme_snapshot'),
|
|
'linkedProgrammeItems' => (array) $room->setting('linked_programme_items', []),
|
|
]);
|
|
}
|
|
|
|
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 requestSpeak(Request $request, Session $session): JsonResponse
|
|
{
|
|
$participant = $this->currentParticipant($request, $session);
|
|
$speak = app(\App\Services\Meet\SpeakAccessService::class);
|
|
|
|
return response()->json([
|
|
'participant' => $speak->requestSpeak($participant),
|
|
]);
|
|
}
|
|
|
|
public function cancelSpeakRequest(Request $request, Session $session): JsonResponse
|
|
{
|
|
$participant = $this->currentParticipant($request, $session);
|
|
$speak = app(\App\Services\Meet\SpeakAccessService::class);
|
|
|
|
return response()->json([
|
|
'participant' => $speak->cancelSpeakRequest($participant),
|
|
]);
|
|
}
|
|
|
|
public function grantSpeak(Request $request, Session $session, Participant $participant): JsonResponse
|
|
{
|
|
$actor = $this->currentParticipant($request, $session);
|
|
abort_unless($participant->session_id === $session->id, 404);
|
|
|
|
$speak = app(\App\Services\Meet\SpeakAccessService::class);
|
|
|
|
return response()->json([
|
|
'participant' => $speak->grantSpeak($participant, $actor),
|
|
]);
|
|
}
|
|
|
|
public function revokeSpeak(Request $request, Session $session, Participant $participant): JsonResponse
|
|
{
|
|
$actor = $this->currentParticipant($request, $session);
|
|
abort_unless($participant->session_id === $session->id, 404);
|
|
|
|
$speak = app(\App\Services\Meet\SpeakAccessService::class);
|
|
|
|
return response()->json([
|
|
'participant' => $speak->revokeSpeak($participant, $actor),
|
|
]);
|
|
}
|
|
|
|
public function dismissSpeakRequest(Request $request, Session $session, Participant $participant): JsonResponse
|
|
{
|
|
$actor = $this->currentParticipant($request, $session);
|
|
abort_unless($participant->session_id === $session->id, 404);
|
|
|
|
$speak = app(\App\Services\Meet\SpeakAccessService::class);
|
|
|
|
return response()->json([
|
|
'participant' => $speak->dismissSpeakRequest($participant, $actor),
|
|
]);
|
|
}
|
|
|
|
public function inviteSpaceSpeaker(Request $request, Session $session, Participant $participant): JsonResponse
|
|
{
|
|
$actor = $this->currentParticipant($request, $session);
|
|
abort_unless($participant->session_id === $session->id, 404);
|
|
abort_unless($session->room->isSpace(), 404);
|
|
abort_unless($actor->isHost() || ($request->user() && $request->user()->ownerRef() === $session->room->host_user_ref), 403);
|
|
|
|
$updated = app(SpaceService::class)->inviteToSpeak($session->room, $participant);
|
|
$avatars = ParticipantPresenter::avatarMap($session->participants()->get(), $session->room->host_user_ref);
|
|
|
|
return response()->json([
|
|
'participant' => ParticipantPresenter::toArray($updated, $avatars, $session->room->host_user_ref),
|
|
]);
|
|
}
|
|
|
|
public function acceptSpaceSpeakInvitation(Request $request, Session $session): JsonResponse
|
|
{
|
|
$participant = $this->currentParticipant($request, $session);
|
|
abort_unless($session->room->isSpace(), 404);
|
|
|
|
$updated = app(SpaceService::class)->acceptSpeakInvitation($session->room, $participant);
|
|
$avatars = ParticipantPresenter::avatarMap($session->participants()->get(), $session->room->host_user_ref);
|
|
|
|
return response()->json([
|
|
'participant' => ParticipantPresenter::toArray($updated, $avatars, $session->room->host_user_ref),
|
|
]);
|
|
}
|
|
|
|
public function declineSpaceSpeakInvitation(Request $request, Session $session): JsonResponse
|
|
{
|
|
$participant = $this->currentParticipant($request, $session);
|
|
abort_unless($session->room->isSpace(), 404);
|
|
|
|
$updated = app(SpaceService::class)->declineSpeakInvitation($session->room, $participant);
|
|
$avatars = ParticipantPresenter::avatarMap($session->participants()->get(), $session->room->host_user_ref);
|
|
|
|
return response()->json([
|
|
'participant' => ParticipantPresenter::toArray($updated, $avatars, $session->room->host_user_ref),
|
|
]);
|
|
}
|
|
|
|
public function dismissSpaceSpeakInvitation(Request $request, Session $session, Participant $participant): JsonResponse
|
|
{
|
|
$actor = $this->currentParticipant($request, $session);
|
|
abort_unless($participant->session_id === $session->id, 404);
|
|
abort_unless($session->room->isSpace(), 404);
|
|
abort_unless($actor->isHost() || ($request->user() && $request->user()->ownerRef() === $session->room->host_user_ref), 403);
|
|
|
|
$updated = app(SpaceService::class)->dismissSpeakInvitation($session->room, $participant);
|
|
$avatars = ParticipantPresenter::avatarMap($session->participants()->get(), $session->room->host_user_ref);
|
|
|
|
return response()->json([
|
|
'participant' => ParticipantPresenter::toArray($updated, $avatars, $session->room->host_user_ref),
|
|
]);
|
|
}
|
|
|
|
public function promoteSpaceCoHost(Request $request, Session $session, Participant $participant): JsonResponse
|
|
{
|
|
$actor = $this->currentParticipant($request, $session);
|
|
abort_unless($participant->session_id === $session->id, 404);
|
|
abort_unless($session->room->isSpace(), 404);
|
|
abort_unless($actor->isHost() || ($request->user() && $request->user()->ownerRef() === $session->room->host_user_ref), 403);
|
|
|
|
$updated = app(SpaceService::class)->promoteToCoHost($session->room, $participant);
|
|
$avatars = ParticipantPresenter::avatarMap($session->participants()->get(), $session->room->host_user_ref);
|
|
|
|
return response()->json([
|
|
'participant' => ParticipantPresenter::toArray($updated, $avatars, $session->room->host_user_ref),
|
|
]);
|
|
}
|
|
|
|
public function demoteSpaceSpeaker(Request $request, Session $session, Participant $participant): JsonResponse
|
|
{
|
|
$actor = $this->currentParticipant($request, $session);
|
|
abort_unless($participant->session_id === $session->id, 404);
|
|
abort_unless($session->room->isSpace(), 404);
|
|
abort_unless($actor->isHost() || ($request->user() && $request->user()->ownerRef() === $session->room->host_user_ref), 403);
|
|
|
|
$updated = app(SpaceService::class)->demoteFromSpeaker($session->room, $participant);
|
|
$avatars = ParticipantPresenter::avatarMap($session->participants()->get(), $session->room->host_user_ref);
|
|
|
|
return response()->json([
|
|
'participant' => ParticipantPresenter::toArray($updated, $avatars, $session->room->host_user_ref),
|
|
]);
|
|
}
|
|
|
|
public function sendMessage(Request $request, Session $session): JsonResponse
|
|
{
|
|
abort_unless(app(SpaceService::class)->allowsRoomFeature($session->room, 'chat'), 422);
|
|
|
|
$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()) {
|
|
$townHall = app(TownHallService::class);
|
|
$liveSession = $townHall->successorLiveSession($session);
|
|
|
|
if ($liveSession) {
|
|
$participantUuid = $request->session()->get("meet.participant.{$session->uuid}");
|
|
|
|
if ($participantUuid) {
|
|
$greenParticipant = Participant::where('uuid', $participantUuid)
|
|
->where('session_id', $session->id)
|
|
->first();
|
|
|
|
if ($greenParticipant && $this->sessions->transitionGreenRoomParticipant($request, $greenParticipant, $liveSession)) {
|
|
return response()->json([
|
|
'ended' => true,
|
|
'redirect' => route('meet.room', $liveSession),
|
|
'transition' => 'go_live',
|
|
]);
|
|
}
|
|
}
|
|
}
|
|
|
|
return response()->json([
|
|
'ended' => true,
|
|
'redirect' => route('meet.ended', $session),
|
|
]);
|
|
}
|
|
|
|
$participant = $this->currentParticipant($request, $session);
|
|
|
|
app(BreakoutService::class)->closeExpired($session);
|
|
|
|
$participant->refresh();
|
|
|
|
$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,
|
|
]),
|
|
'media' => $this->sessions->mediaSessionState($participant),
|
|
'breakouts_active' => app(BreakoutService::class)->hasActiveBreakouts($session),
|
|
], app(StageLayoutService::class)->config($session->room)));
|
|
}
|
|
|
|
public function mediaToken(Request $request, Session $session): JsonResponse
|
|
{
|
|
abort_unless($session->isLive(), 404);
|
|
|
|
$participant = $this->currentParticipant($request, $session);
|
|
|
|
return response()->json(array_merge(
|
|
['token' => $this->sessions->generateMediaToken($participant)],
|
|
$this->sessions->mediaSessionState($participant->fresh()),
|
|
['breakouts_active' => app(BreakoutService::class)->hasActiveBreakouts($session)],
|
|
));
|
|
}
|
|
|
|
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->recordingHost($request, $session);
|
|
|
|
$recording = $session->recordings()->where('status', 'recording')->firstOrFail();
|
|
$actorRef = $participant->user_ref ?? $participant->uuid;
|
|
|
|
if ($request->boolean('failed')) {
|
|
$recording = $this->recordings->fail(
|
|
$recording,
|
|
(string) $request->input('reason', 'Recording capture did not complete.'),
|
|
$actorRef,
|
|
);
|
|
|
|
return response()->json([
|
|
'status' => 'failed',
|
|
'recording_uuid' => $recording->uuid,
|
|
]);
|
|
}
|
|
|
|
$recording = $this->recordings->stop($recording, $actorRef);
|
|
|
|
return response()->json([
|
|
'status' => 'processing',
|
|
'recording_uuid' => $recording->uuid,
|
|
]);
|
|
}
|
|
|
|
public function failRecording(Request $request, Session $session, Recording $recording): JsonResponse
|
|
{
|
|
$participant = $this->recordingHost($request, $session);
|
|
abort_unless($recording->session_id === $session->id, 404);
|
|
abort_if(! in_array($recording->status, ['recording', 'processing'], true), 422);
|
|
|
|
$recording = $this->recordings->fail(
|
|
$recording,
|
|
(string) $request->input('reason', 'Recording capture did not complete.'),
|
|
$participant->user_ref ?? $participant->uuid,
|
|
);
|
|
|
|
return response()->json([
|
|
'status' => $recording->status,
|
|
'recording_uuid' => $recording->uuid,
|
|
]);
|
|
}
|
|
|
|
public function uploadRecording(Request $request, Session $session, Recording $recording): JsonResponse
|
|
{
|
|
$participant = $this->recordingHost($request, $session);
|
|
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
|
|
{
|
|
abort_unless(app(SpaceService::class)->allowsRoomFeature($session->room, 'lock'), 422);
|
|
|
|
$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
|
|
{
|
|
abort_unless(app(SpaceService::class)->allowsRoomFeature($session->room, 'lock'), 422);
|
|
|
|
$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 $this->sessions->syncEventsSpeakerRole(
|
|
$request,
|
|
Participant::where('uuid', $participantUuid)
|
|
->where('session_id', $session->id)
|
|
->firstOrFail()
|
|
);
|
|
}
|
|
|
|
/** Host/co-host in the room session, or the room host after the meeting ended. */
|
|
protected function recordingHost(Request $request, Session $session): Participant
|
|
{
|
|
$participantUuid = $request->session()->get("meet.participant.{$session->uuid}");
|
|
if ($participantUuid) {
|
|
$participant = Participant::query()
|
|
->where('uuid', $participantUuid)
|
|
->where('session_id', $session->id)
|
|
->first();
|
|
|
|
if ($participant?->isHost()) {
|
|
return $this->sessions->syncEventsSpeakerRole($request, $participant);
|
|
}
|
|
}
|
|
|
|
$user = $request->user();
|
|
abort_unless($user, 403);
|
|
|
|
$hostParticipant = $session->participants()
|
|
->where('user_ref', $user->ownerRef())
|
|
->whereIn('role', ['host', 'co_host'])
|
|
->orderByRaw("case when role = 'host' then 0 else 1 end")
|
|
->first();
|
|
|
|
abort_unless($hostParticipant, 403);
|
|
|
|
return $hostParticipant;
|
|
}
|
|
}
|