Deploy Ladill Meet / deploy (push) Successful in 1m29s
Split town_hall into a Conferences sidebar flow and Spaces-style Rooms with host/speaker/listener roles; enforce audio-only LiveKit UI for rooms; show schedule icon on mobile meetings index. Co-authored-by: Cursor <cursoragent@cursor.com>
317 lines
9.7 KiB
PHP
317 lines
9.7 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Meet;
|
|
|
|
use App\Models\Participant;
|
|
use App\Models\Room;
|
|
use App\Models\Session;
|
|
use App\Models\User;
|
|
use App\Services\Meet\Media\MediaProviderInterface;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Str;
|
|
|
|
class SessionService
|
|
{
|
|
public function __construct(
|
|
protected MediaProviderInterface $media,
|
|
protected RoomService $rooms,
|
|
) {}
|
|
|
|
public function start(Room $room, User $host): Session
|
|
{
|
|
$existing = $room->activeSession();
|
|
if ($existing) {
|
|
return $existing;
|
|
}
|
|
|
|
app(LicenseService::class)->assertCanStartSession($room->organization, $room->branch_id, $room);
|
|
|
|
$session = Session::create([
|
|
'owner_ref' => $room->owner_ref,
|
|
'room_id' => $room->id,
|
|
'media_room_name' => $this->rooms->mediaRoomName($room),
|
|
'status' => 'live',
|
|
'started_at' => now(),
|
|
]);
|
|
|
|
$room->update(['status' => 'live']);
|
|
|
|
$this->media->createRoom($session->media_room_name);
|
|
|
|
$this->joinParticipant($session, $host, 'host');
|
|
|
|
if ($room->setting('auto_record', false)) {
|
|
app(RecordingService::class)->start($session, $host);
|
|
}
|
|
|
|
app(MeetNotificationService::class)->meetingStarted($session, $host);
|
|
|
|
app(\App\Services\Integrations\WebhookDispatcher::class)->meetingStarted($session);
|
|
|
|
AuditLogger::record($room->owner_ref, 'session.started', $room->organization_id, $host->ownerRef(), Session::class, $session->id);
|
|
|
|
return $session;
|
|
}
|
|
|
|
public function end(Session $session, string $actorRef): Session
|
|
{
|
|
$session->update([
|
|
'status' => 'ended',
|
|
'ended_at' => now(),
|
|
]);
|
|
|
|
$session->room->update(['status' => 'ended']);
|
|
|
|
$session->participants()
|
|
->whereIn('status', ['joined', 'waiting'])
|
|
->update(['status' => 'left', 'left_at' => now()]);
|
|
|
|
foreach ($session->recordings()->where('status', 'recording')->get() as $recording) {
|
|
app(RecordingService::class)->stop($recording, $actorRef);
|
|
}
|
|
|
|
if ($session->room->isRecurring()) {
|
|
app(RecurringMeetingService::class)->spawnNextOccurrences($session->room, 1);
|
|
}
|
|
|
|
AuditLogger::record($session->owner_ref, 'session.ended', $session->room->organization_id, $actorRef, Session::class, $session->id);
|
|
|
|
app(\App\Services\Integrations\WebhookDispatcher::class)->meetingEnded($session);
|
|
|
|
app(UsageMeteringService::class)->recordSessionUsage($session);
|
|
|
|
return $session;
|
|
}
|
|
|
|
public function joinParticipant(
|
|
Session $session,
|
|
?User $user,
|
|
string $role,
|
|
?string $displayName = null,
|
|
?string $email = null,
|
|
string $status = 'joined',
|
|
): Participant {
|
|
$userRef = $user?->ownerRef();
|
|
$existing = $this->findExistingParticipant($session, $userRef, $email);
|
|
|
|
if ($existing) {
|
|
$updates = [
|
|
'left_at' => null,
|
|
'display_name' => $displayName ?? $existing->display_name,
|
|
'ip_address' => request()?->ip(),
|
|
];
|
|
|
|
if ($existing->status === 'waiting') {
|
|
$updates['status'] = 'waiting';
|
|
} else {
|
|
$updates['status'] = 'joined';
|
|
$updates['joined_at'] = $existing->joined_at ?? now();
|
|
}
|
|
|
|
$existing->update($updates);
|
|
|
|
return $existing;
|
|
}
|
|
|
|
$joinedAt = $status === 'joined' ? now() : null;
|
|
|
|
$participant = Participant::create([
|
|
'owner_ref' => $session->owner_ref,
|
|
'session_id' => $session->id,
|
|
'user_ref' => $userRef,
|
|
'display_name' => $displayName ?? $user?->name ?? 'Guest',
|
|
'email' => $email,
|
|
'role' => $role,
|
|
'status' => $status,
|
|
'joined_at' => $joinedAt,
|
|
'ip_address' => request()?->ip(),
|
|
]);
|
|
|
|
if ($status === 'joined') {
|
|
$this->updatePeakParticipants($session);
|
|
}
|
|
|
|
if ($status === 'joined') {
|
|
AuditLogger::record(
|
|
$session->owner_ref,
|
|
'participant.joined',
|
|
$session->room->organization_id,
|
|
$userRef,
|
|
Participant::class,
|
|
$participant->id,
|
|
);
|
|
}
|
|
|
|
return $participant;
|
|
}
|
|
|
|
public function admitParticipant(Participant $participant): Participant
|
|
{
|
|
abort_unless($participant->status === 'waiting', 422);
|
|
|
|
$participant->update([
|
|
'status' => 'joined',
|
|
'joined_at' => now(),
|
|
'left_at' => null,
|
|
]);
|
|
|
|
$this->updatePeakParticipants($participant->session);
|
|
|
|
AuditLogger::record(
|
|
$participant->owner_ref,
|
|
'participant.joined',
|
|
$participant->session->room->organization_id,
|
|
$participant->user_ref,
|
|
Participant::class,
|
|
$participant->id,
|
|
);
|
|
|
|
return $participant->fresh();
|
|
}
|
|
|
|
public function denyParticipant(Participant $participant): Participant
|
|
{
|
|
abort_unless($participant->status === 'waiting', 422);
|
|
|
|
$participant->update([
|
|
'status' => 'removed',
|
|
'left_at' => now(),
|
|
]);
|
|
|
|
AuditLogger::record(
|
|
$participant->owner_ref,
|
|
'participant.removed',
|
|
$participant->session->room->organization_id,
|
|
$participant->user_ref,
|
|
Participant::class,
|
|
$participant->id,
|
|
);
|
|
|
|
return $participant->fresh();
|
|
}
|
|
|
|
protected function findExistingParticipant(Session $session, ?string $userRef, ?string $email): ?Participant
|
|
{
|
|
$active = fn ($q) => $q->whereIn('status', ['invited', 'waiting', 'joined']);
|
|
|
|
if ($userRef) {
|
|
return $session->participants()
|
|
->where('user_ref', $userRef)
|
|
->where($active)
|
|
->first();
|
|
}
|
|
|
|
if ($email) {
|
|
return $session->participants()
|
|
->whereNull('user_ref')
|
|
->where('email', $email)
|
|
->where($active)
|
|
->first();
|
|
}
|
|
|
|
$guestUuid = (string) request()?->session()?->get("meet.participant.{$session->uuid}", '');
|
|
if ($guestUuid === '') {
|
|
return null;
|
|
}
|
|
|
|
return $session->participants()
|
|
->where('uuid', $guestUuid)
|
|
->whereNull('user_ref')
|
|
->where($active)
|
|
->first();
|
|
}
|
|
|
|
public function rememberParticipantSession(Request $request, Participant $participant, ?Session $session = null): void
|
|
{
|
|
$session ??= $participant->session;
|
|
$request->session()->put("meet.participant.{$session->uuid}", $participant->uuid);
|
|
}
|
|
|
|
public function joinedParticipantForUser(Session $session, User $user): ?Participant
|
|
{
|
|
return $session->participants()
|
|
->where('user_ref', $user->ownerRef())
|
|
->whereIn('status', ['invited', 'waiting', 'joined'])
|
|
->first();
|
|
}
|
|
|
|
public function leaveParticipant(Participant $participant): Participant
|
|
{
|
|
$participant->update([
|
|
'status' => 'left',
|
|
'left_at' => now(),
|
|
'hand_raised' => false,
|
|
]);
|
|
|
|
AuditLogger::record(
|
|
$participant->owner_ref,
|
|
'participant.left',
|
|
$participant->session->room->organization_id,
|
|
$participant->user_ref,
|
|
Participant::class,
|
|
$participant->id,
|
|
);
|
|
|
|
return $participant;
|
|
}
|
|
|
|
public function generateMediaToken(Participant $participant): string
|
|
{
|
|
$session = $participant->session;
|
|
$room = $session->room;
|
|
$isHost = $participant->isHost();
|
|
$breakoutService = app(BreakoutService::class);
|
|
$webinarService = app(WebinarService::class);
|
|
$spaceService = app(\App\Services\Meet\SpaceService::class);
|
|
$mediaRoom = $breakoutService->mediaRoomForParticipant($participant);
|
|
$canPublish = ($room->isSpace()
|
|
? $spaceService->canPublish($room, $participant)
|
|
: $webinarService->canPublish($room, $participant)) && $participant->canPublishMedia();
|
|
|
|
return $this->media->generateToken(
|
|
$mediaRoom,
|
|
$participant->uuid,
|
|
$participant->display_name,
|
|
[
|
|
'can_publish' => $canPublish,
|
|
'can_subscribe' => true,
|
|
'can_publish_data' => true,
|
|
'room_admin' => $isHost,
|
|
],
|
|
);
|
|
}
|
|
|
|
public function getOrStartSession(Room $room, User $host): Session
|
|
{
|
|
if ($room->activeSession()) {
|
|
return $room->activeSession();
|
|
}
|
|
|
|
return $this->start($room, $host);
|
|
}
|
|
|
|
public function lock(Session $session, string $actorRef): Session
|
|
{
|
|
$session->update(['is_locked' => true, 'locked_at' => now()]);
|
|
AuditLogger::record($session->owner_ref, 'meeting.locked', $session->room->organization_id, $actorRef, Session::class, $session->id);
|
|
|
|
return $session;
|
|
}
|
|
|
|
public function unlock(Session $session, string $actorRef): Session
|
|
{
|
|
$session->update(['is_locked' => false, 'locked_at' => null]);
|
|
AuditLogger::record($session->owner_ref, 'meeting.unlocked', $session->room->organization_id, $actorRef, Session::class, $session->id);
|
|
|
|
return $session;
|
|
}
|
|
|
|
protected function updatePeakParticipants(Session $session): void
|
|
{
|
|
$count = $session->participants()->where('status', 'joined')->count();
|
|
if ($count > $session->peak_participants) {
|
|
$session->update(['peak_participants' => $count]);
|
|
}
|
|
}
|
|
}
|