Files
ladill-meet/app/Services/Meet/SessionService.php
T
isaaccladandCursor 63af522380
Deploy Ladill Meet / deploy (push) Successful in 1m29s
Add Conferences and audio-only Rooms, plus mobile schedule icon.
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>
2026-07-01 19:16:54 +00:00

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]);
}
}
}