Deploy Ladill Meet / deploy (push) Successful in 46s
Store programme_snapshot from the link-room response locally and merge existing room settings on service API updates. Co-authored-by: Cursor <cursoragent@cursor.com>
290 lines
10 KiB
PHP
290 lines
10 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api\V1;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Organization;
|
|
use App\Models\Room;
|
|
use App\Models\User;
|
|
use App\Services\Integrations\WebhookDispatcher;
|
|
use App\Services\Meet\CalendarService;
|
|
use App\Services\Meet\InvitationService;
|
|
use App\Services\Meet\RoomService;
|
|
use App\Services\Meet\SessionService;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Validation\Rule;
|
|
|
|
class ServiceRoomController extends Controller
|
|
{
|
|
private const SERVICE_TYPES = ['instant', 'scheduled', 'town_hall', 'webinar'];
|
|
|
|
public function __construct(
|
|
protected RoomService $rooms,
|
|
protected SessionService $sessions,
|
|
protected InvitationService $invitations,
|
|
protected CalendarService $calendar,
|
|
protected WebhookDispatcher $webhooks,
|
|
) {}
|
|
|
|
public function store(Request $request): JsonResponse
|
|
{
|
|
$validated = $this->validateRoomPayload($request);
|
|
|
|
$organization = $this->resolveOrganization($validated);
|
|
abort_unless($organization->owner_ref === $validated['owner_ref'], 403);
|
|
|
|
$roomUuid = trim((string) ($validated['meet_room_uuid'] ?? ''));
|
|
if ($roomUuid !== '') {
|
|
$existing = Room::query()
|
|
->where('uuid', $roomUuid)
|
|
->where('owner_ref', $validated['owner_ref'])
|
|
->first();
|
|
|
|
if ($existing) {
|
|
return $this->respondWithRoom(
|
|
$this->persistRoomUpdate($existing, $validated, $organization),
|
|
);
|
|
}
|
|
}
|
|
|
|
$entityId = (string) ($validated['source']['entity_id'] ?? '');
|
|
if ($entityId !== '') {
|
|
$existing = Room::query()
|
|
->forSource((string) $validated['source']['app'], $entityId, $validated['owner_ref'])
|
|
->first();
|
|
|
|
if ($existing) {
|
|
return $this->respondWithRoom(
|
|
$this->persistRoomUpdate($existing, $validated, $organization),
|
|
);
|
|
}
|
|
}
|
|
|
|
return $this->respondWithRoom(
|
|
$this->persistRoomCreate($validated, $organization),
|
|
201,
|
|
);
|
|
}
|
|
|
|
public function lookup(Request $request): JsonResponse
|
|
{
|
|
$validated = $request->validate([
|
|
'source_app' => ['required', 'string'],
|
|
'entity_id' => ['required', 'string'],
|
|
'owner_ref' => ['nullable', 'string'],
|
|
]);
|
|
|
|
$room = Room::query()
|
|
->forSource(
|
|
$validated['source_app'],
|
|
$validated['entity_id'],
|
|
$validated['owner_ref'] ?? null,
|
|
)
|
|
->first();
|
|
|
|
if (! $room) {
|
|
return response()->json(['error' => 'Room not found.'], 404);
|
|
}
|
|
|
|
return $this->respondWithRoom($room);
|
|
}
|
|
|
|
public function show(Room $room): JsonResponse
|
|
{
|
|
return $this->respondWithRoom($room->load('sessions'));
|
|
}
|
|
|
|
public function update(Request $request, Room $room): JsonResponse
|
|
{
|
|
$validated = $this->validateRoomPayload($request, partial: true);
|
|
abort_unless($room->owner_ref === ($validated['owner_ref'] ?? $room->owner_ref), 403);
|
|
|
|
$organization = isset($validated['organization_id'])
|
|
? Organization::findOrFail($validated['organization_id'])
|
|
: $room->organization;
|
|
|
|
return $this->respondWithRoom(
|
|
$this->persistRoomUpdate($room, $validated, $organization),
|
|
);
|
|
}
|
|
|
|
public function cancel(Request $request, Room $room): JsonResponse
|
|
{
|
|
$validated = $request->validate([
|
|
'owner_ref' => ['required', 'string'],
|
|
'host_user_ref' => ['nullable', 'string'],
|
|
]);
|
|
|
|
abort_unless($room->owner_ref === $validated['owner_ref'], 403);
|
|
|
|
$host = User::where('public_id', $validated['host_user_ref'] ?? $room->host_user_ref)->first();
|
|
$this->rooms->cancel($room, $validated['owner_ref'], $host);
|
|
|
|
return $this->respondWithRoom($room->fresh());
|
|
}
|
|
|
|
public function start(Request $request, Room $room): JsonResponse
|
|
{
|
|
$host = User::where('public_id', $request->input('host_user_ref', $room->host_user_ref))->firstOrFail();
|
|
abort_unless($room->host_user_ref === $host->ownerRef(), 403);
|
|
|
|
$session = $this->sessions->start($room, $host);
|
|
|
|
return response()->json([
|
|
'session' => $session,
|
|
'room_url' => route('meet.room', $session),
|
|
'join_url' => $room->joinUrl(),
|
|
]);
|
|
}
|
|
|
|
/** @return array<string, mixed> */
|
|
private function validateRoomPayload(Request $request, bool $partial = false): array
|
|
{
|
|
$required = $partial ? 'sometimes' : 'required';
|
|
|
|
return $request->validate([
|
|
'owner_ref' => [$required, 'string'],
|
|
'organization_id' => ['nullable', 'integer', 'exists:meet_organizations,id'],
|
|
'host_user_ref' => [$partial ? 'sometimes' : 'required', 'string'],
|
|
'title' => [$partial ? 'sometimes' : 'required', 'string', 'max:255'],
|
|
'description' => ['nullable', 'string'],
|
|
'type' => ['nullable', 'string', Rule::in(self::SERVICE_TYPES)],
|
|
'scheduled_at' => ['nullable', 'date'],
|
|
'duration_minutes' => ['nullable', 'integer', 'min:15', 'max:480'],
|
|
'timezone' => ['nullable', 'timezone'],
|
|
'passcode' => ['nullable', 'string', 'max:20'],
|
|
'settings' => ['nullable', 'array'],
|
|
'source' => [$partial ? 'sometimes' : 'required', 'array'],
|
|
'source.app' => [$partial ? 'sometimes' : 'required', 'string'],
|
|
'source.entity_type' => ['nullable', 'string'],
|
|
'source.entity_id' => ['nullable', 'string'],
|
|
'source.event_id' => ['nullable', 'string'],
|
|
'invite_emails' => ['nullable', 'array'],
|
|
'invite_emails.*' => ['email'],
|
|
'meet_room_uuid' => ['nullable', 'string', 'max:64'],
|
|
]);
|
|
}
|
|
|
|
/** @param array<string, mixed> $validated */
|
|
private function resolveOrganization(array $validated): Organization
|
|
{
|
|
return isset($validated['organization_id'])
|
|
? Organization::findOrFail($validated['organization_id'])
|
|
: Organization::owned($validated['owner_ref'])->firstOrFail();
|
|
}
|
|
|
|
/** @param array<string, mixed> $validated */
|
|
private function persistRoomCreate(array $validated, Organization $organization): Room
|
|
{
|
|
$host = User::where('public_id', $validated['host_user_ref'])->firstOrFail();
|
|
$type = $this->resolveType($validated);
|
|
|
|
$payload = array_merge($validated, [
|
|
'type' => $type,
|
|
'status' => 'scheduled',
|
|
'settings' => $this->mergeSettingsForType($type, $validated['settings'] ?? []),
|
|
]);
|
|
|
|
$room = $type === 'instant' && empty($validated['scheduled_at'])
|
|
? $this->rooms->createInstant($host, $organization, $payload)
|
|
: $this->rooms->create($host, $organization, $payload);
|
|
|
|
$this->afterRoomPersisted($room, $validated, $host);
|
|
|
|
return $room->fresh();
|
|
}
|
|
|
|
/** @param array<string, mixed> $validated */
|
|
private function persistRoomUpdate(Room $room, array $validated, Organization $organization): Room
|
|
{
|
|
$hostRef = $validated['host_user_ref'] ?? $room->host_user_ref;
|
|
$host = User::where('public_id', $hostRef)->firstOrFail();
|
|
|
|
$update = array_filter([
|
|
'title' => $validated['title'] ?? null,
|
|
'description' => array_key_exists('description', $validated) ? $validated['description'] : null,
|
|
'scheduled_at' => $validated['scheduled_at'] ?? null,
|
|
'duration_minutes' => $validated['duration_minutes'] ?? null,
|
|
'timezone' => $validated['timezone'] ?? null,
|
|
'passcode' => array_key_exists('passcode', $validated) ? $validated['passcode'] : null,
|
|
'host_user_ref' => $hostRef,
|
|
'source' => $validated['source'] ?? null,
|
|
], fn ($value) => $value !== null);
|
|
|
|
if (isset($validated['settings'])) {
|
|
$type = $this->resolveType(array_merge(['type' => $room->type], $validated));
|
|
$update['settings'] = $this->mergeSettingsForType(
|
|
$type,
|
|
array_merge($room->settings ?? [], $validated['settings']),
|
|
);
|
|
}
|
|
|
|
$room = $this->rooms->update($room, $update, $host);
|
|
$this->afterRoomPersisted($room, $validated, $host);
|
|
|
|
return $room;
|
|
}
|
|
|
|
/** @param array<string, mixed> $validated */
|
|
private function afterRoomPersisted(Room $room, array $validated, User $host): void
|
|
{
|
|
if (! empty($validated['invite_emails'])) {
|
|
$invites = collect($validated['invite_emails'])->map(fn ($email) => ['email' => $email])->all();
|
|
$this->invitations->inviteToRoom($room, $invites, $host);
|
|
}
|
|
|
|
if ($room->scheduled_at) {
|
|
$this->calendar->syncRoomCreated($room, $host);
|
|
}
|
|
}
|
|
|
|
/** @param array<string, mixed> $validated */
|
|
private function resolveType(array $validated): string
|
|
{
|
|
if (! empty($validated['type'])) {
|
|
return (string) $validated['type'];
|
|
}
|
|
|
|
return empty($validated['scheduled_at']) ? 'instant' : 'scheduled';
|
|
}
|
|
|
|
/** @param array<string, mixed> $settings */
|
|
private function mergeSettingsForType(string $type, array $settings): array
|
|
{
|
|
$defaults = match ($type) {
|
|
'town_hall' => [
|
|
'waiting_room' => true,
|
|
'mute_on_join' => true,
|
|
'green_room' => true,
|
|
'stage_layout' => 'plenary',
|
|
'stage_mode' => true,
|
|
'panel_discussions' => false,
|
|
'panels' => [],
|
|
'presenter_refs' => [],
|
|
],
|
|
'webinar' => [
|
|
'waiting_room' => true,
|
|
'mute_on_join' => true,
|
|
'webinar_auto_approve' => false,
|
|
'stage_layout' => 'plenary',
|
|
'stage_mode' => true,
|
|
'panel_discussions' => false,
|
|
'panels' => [],
|
|
],
|
|
default => [],
|
|
};
|
|
|
|
return array_merge(config('meet.default_settings', []), $defaults, $settings);
|
|
}
|
|
|
|
private function respondWithRoom(Room $room, int $status = 200): JsonResponse
|
|
{
|
|
return response()->json([
|
|
'room' => $room->loadMissing('sessions'),
|
|
'join_url' => $room->joinUrl(),
|
|
'embed_url' => route('meet.join', $room),
|
|
], $status);
|
|
}
|
|
}
|