Deploy Ladill Meet / deploy (push) Successful in 42s
Enforce invite-only room access, validate invite emails on create, assign attendee role for listener invitations, and add invite UI on the room show page. Redesign meetings, rooms, conferences, and webinars indexes to match the Events hero, stats cards, and icon list layout. Co-authored-by: Cursor <cursoragent@cursor.com>
257 lines
9.8 KiB
PHP
257 lines
9.8 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Meet;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Http\Controllers\Meet\Concerns\BuildsMeetIndexPage;
|
|
use App\Http\Controllers\Meet\Concerns\ScopesToAccount;
|
|
use App\Models\Room;
|
|
use App\Services\Meet\AttendanceService;
|
|
use App\Services\Meet\AvailabilityService;
|
|
use App\Services\Meet\CalendarService;
|
|
use App\Services\Meet\IcalService;
|
|
use App\Services\Meet\InvitationService;
|
|
use App\Services\Meet\RecurringMeetingService;
|
|
use App\Services\Meet\RoomService;
|
|
use App\Services\Meet\SessionService;
|
|
use chillerlan\QRCode\QRCode;
|
|
use chillerlan\QRCode\QROptions;
|
|
use Illuminate\Http\RedirectResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Http\Response;
|
|
use Illuminate\View\View;
|
|
|
|
class RoomController extends Controller
|
|
{
|
|
use BuildsMeetIndexPage, ScopesToAccount;
|
|
|
|
public function __construct(
|
|
protected RoomService $rooms,
|
|
protected SessionService $sessions,
|
|
protected RecurringMeetingService $recurring,
|
|
protected InvitationService $invitations,
|
|
protected CalendarService $calendar,
|
|
protected AvailabilityService $availability,
|
|
protected IcalService $ical,
|
|
protected AttendanceService $attendance,
|
|
) {}
|
|
|
|
public function index(Request $request): View
|
|
{
|
|
$this->authorizeAbility($request, 'meetings.view');
|
|
$organization = $this->organization($request);
|
|
$owner = $this->ownerRef($request);
|
|
|
|
$query = Room::owned($owner)->where('organization_id', $organization->id)
|
|
->whereNotIn('type', ['webinar', 'town_hall', 'space']);
|
|
$this->scopeToBranch($request, $query);
|
|
|
|
$rooms = $query->orderByDesc('scheduled_at')->paginate(20);
|
|
|
|
$stats = $this->meetIndexStats($request, $query, 'Meetings', 'Live now');
|
|
$items = $this->meetIndexItems($rooms, fn (Room $room) => route('meet.rooms.show', $room));
|
|
|
|
return view('meet.rooms.index', compact('rooms', 'organization', 'stats', 'items'));
|
|
}
|
|
|
|
public function create(Request $request): View
|
|
{
|
|
$this->authorizeAbility($request, 'meetings.create');
|
|
$organization = $this->organization($request);
|
|
|
|
return view('meet.rooms.create', [
|
|
'organization' => $organization,
|
|
'timezones' => timezone_identifiers_list(),
|
|
'recurrenceRules' => config('meet.recurrence_rules'),
|
|
'defaultSettings' => config('meet.default_settings'),
|
|
]);
|
|
}
|
|
|
|
public function store(Request $request): RedirectResponse
|
|
{
|
|
$this->authorizeAbility($request, 'meetings.create');
|
|
$organization = $this->organization($request);
|
|
|
|
$validated = $request->validate([
|
|
'title' => ['required', 'string', 'max:255'],
|
|
'description' => ['nullable', 'string', 'max:2000'],
|
|
'type' => ['nullable', 'string', 'in:scheduled'],
|
|
'scheduled_at' => ['nullable', 'date'],
|
|
'duration_minutes' => ['nullable', 'integer', 'min:15', 'max:480'],
|
|
'timezone' => ['nullable', 'timezone'],
|
|
'passcode' => ['nullable', 'string', 'max:20'],
|
|
'recurrence_rule' => ['nullable', 'string', 'in:'.implode(',', array_keys(config('meet.recurrence_rules')))],
|
|
'recurrence_interval' => ['nullable', 'integer', 'min:1', 'max:12'],
|
|
'recurrence_until' => ['nullable', 'date'],
|
|
'invite_emails' => ['nullable', 'string'],
|
|
'waiting_room' => ['sometimes', 'boolean'],
|
|
'join_before_host' => ['sometimes', 'boolean'],
|
|
'mute_on_join' => ['sometimes', 'boolean'],
|
|
'auto_record' => ['sometimes', 'boolean'],
|
|
'live_captions' => ['sometimes', 'boolean'],
|
|
'webinar_auto_approve' => ['sometimes', 'boolean'],
|
|
]);
|
|
|
|
$settings = [
|
|
'waiting_room' => $request->boolean('waiting_room', true),
|
|
'join_before_host' => $request->boolean('join_before_host'),
|
|
'mute_on_join' => $request->boolean('mute_on_join', true),
|
|
'auto_record' => $request->boolean('auto_record'),
|
|
'live_captions' => $request->boolean('live_captions'),
|
|
'webinar_auto_approve' => $request->boolean('webinar_auto_approve'),
|
|
'stage_mode' => false,
|
|
];
|
|
|
|
$data = array_merge($validated, [
|
|
'settings' => $settings,
|
|
'timezone' => $validated['timezone'] ?? $organization->timezone,
|
|
]);
|
|
|
|
$isInstant = empty($validated['scheduled_at']);
|
|
|
|
if (! empty($validated['recurrence_rule']) && ! $isInstant) {
|
|
$room = $this->recurring->createSeries($request->user(), $organization, $data);
|
|
} elseif ($isInstant) {
|
|
$room = $this->rooms->createInstant($request->user(), $organization, $data);
|
|
} else {
|
|
$room = $this->rooms->createScheduled($request->user(), $organization, $data);
|
|
}
|
|
|
|
if ($emails = trim((string) ($validated['invite_emails'] ?? ''))) {
|
|
$invites = collect(preg_split('/[\s,;]+/', $emails))
|
|
->filter()
|
|
->map(fn ($email) => ['email' => $email])
|
|
->all();
|
|
$this->invitations->inviteToRoom($room, $invites, $request->user());
|
|
}
|
|
|
|
if (! $isInstant && $room->scheduled_at) {
|
|
$this->calendar->syncRoomCreated($room, $request->user());
|
|
}
|
|
|
|
return redirect()->route('meet.rooms.show', $room)->with('success', 'Meeting created.');
|
|
}
|
|
|
|
public function show(Request $request, Room $room): View|RedirectResponse
|
|
{
|
|
$this->authorizeAbility($request, 'meetings.view');
|
|
$this->authorizeOwner($request, $room);
|
|
|
|
if ($room->isWebinar()) {
|
|
return redirect()->route('meet.webinars.show', $room);
|
|
}
|
|
|
|
if ($room->isConference()) {
|
|
return redirect()->route('meet.conferences.show', $room);
|
|
}
|
|
|
|
if ($room->isSpace()) {
|
|
return redirect()->route('meet.spaces.show', $room);
|
|
}
|
|
|
|
$room->load(['sessions.participants', 'invitations', 'sessions.recordings', 'sessions.aiSummaries', 'sessionFiles', 'webinarRegistrations']);
|
|
|
|
return view('meet.rooms.show', compact('room'));
|
|
}
|
|
|
|
public function start(Request $request, Room $room): RedirectResponse
|
|
{
|
|
$this->authorizeAbility($request, 'meetings.host');
|
|
$this->authorizeOwner($request, $room);
|
|
|
|
if ($room->status === 'cancelled') {
|
|
return redirect()->route('meet.rooms.show', $room)
|
|
->withErrors(['start' => 'Meeting was cancelled.']);
|
|
}
|
|
|
|
if ($room->isTownHall() && $room->setting('green_room')) {
|
|
$session = app(\App\Services\Meet\TownHallService::class)->startGreenRoom($room, $request->user());
|
|
} else {
|
|
$session = $this->sessions->start($room, $request->user());
|
|
}
|
|
|
|
$participant = $this->sessions->joinedParticipantForUser($session, $request->user());
|
|
abort_unless($participant, 500, 'Unable to join as host.');
|
|
|
|
$this->sessions->rememberParticipantSession($request, $participant, $session);
|
|
|
|
return redirect()->route('meet.room', $session);
|
|
}
|
|
|
|
public function cancel(Request $request, Room $room): RedirectResponse
|
|
{
|
|
$this->authorizeAbility($request, 'meetings.manage');
|
|
$this->authorizeOwner($request, $room);
|
|
|
|
$this->rooms->cancel($room, $this->ownerRef($request), $request->user());
|
|
|
|
return redirect()->route('meet.rooms.index')->with('success', 'Meeting cancelled.');
|
|
}
|
|
|
|
public function instant(Request $request): RedirectResponse
|
|
{
|
|
$this->authorizeAbility($request, 'meetings.create');
|
|
$organization = $this->organization($request);
|
|
|
|
$room = $this->rooms->createInstant($request->user(), $organization, [
|
|
'title' => $request->user()->name."'s meeting",
|
|
]);
|
|
|
|
$session = $this->sessions->start($room, $request->user());
|
|
|
|
$participant = $this->sessions->joinedParticipantForUser($session, $request->user());
|
|
abort_unless($participant, 500, 'Unable to join as host.');
|
|
|
|
$this->sessions->rememberParticipantSession($request, $participant, $session);
|
|
|
|
return redirect()->route('meet.room', $session);
|
|
}
|
|
|
|
public function personal(Request $request): RedirectResponse
|
|
{
|
|
$this->authorizeAbility($request, 'meetings.host');
|
|
$organization = $this->organization($request);
|
|
$room = $this->rooms->ensurePersonalRoom($request->user(), $organization);
|
|
|
|
return redirect()->route('meet.rooms.show', $room);
|
|
}
|
|
|
|
public function ical(Request $request, Room $room): Response
|
|
{
|
|
$this->authorizeAbility($request, 'meetings.view');
|
|
$this->authorizeOwner($request, $room);
|
|
|
|
$content = $this->ical->forRoom($room, $request->user());
|
|
|
|
return response($content, 200, [
|
|
'Content-Type' => 'text/calendar; charset=utf-8',
|
|
'Content-Disposition' => 'attachment; filename="meeting-'.$room->uuid.'.ics"',
|
|
]);
|
|
}
|
|
|
|
public function qr(Request $request, Room $room): Response
|
|
{
|
|
$this->authorizeAbility($request, 'meetings.view');
|
|
$this->authorizeOwner($request, $room);
|
|
|
|
$options = new QROptions(['outputType' => QRCode::OUTPUT_IMAGE_PNG, 'scale' => 8]);
|
|
$binary = (new QRCode($options))->render($room->joinUrl());
|
|
|
|
return response($binary, 200, ['Content-Type' => 'image/png']);
|
|
}
|
|
|
|
public function attendance(Request $request, Room $room): Response
|
|
{
|
|
$this->authorizeAbility($request, 'meetings.view');
|
|
$this->authorizeOwner($request, $room);
|
|
|
|
$session = $room->sessions()->latest()->first();
|
|
abort_unless($session, 404);
|
|
|
|
return response($this->attendance->toCsv($session), 200, [
|
|
'Content-Type' => 'text/csv',
|
|
'Content-Disposition' => 'attachment; filename="attendance-'.$room->uuid.'.csv"',
|
|
]);
|
|
}
|
|
}
|