From fea79ff83a7a5a3992fa4e21ec8128d6a5a27ca8 Mon Sep 17 00:00:00 2001 From: Alexandre R D'anzi Date: Fri, 24 Apr 2026 17:29:47 +0200 Subject: [PATCH 1/2] auto create a talk room when creating event with 2 or more attendees --- lib/AppInfo/Application.php | 3 + lib/Dav/BeforeCreateFilePlugin.php | 225 ++++++++++++++++++++++++ lib/Listener/SabrePluginAddListener.php | 38 ++++ 3 files changed, 266 insertions(+) create mode 100644 lib/Dav/BeforeCreateFilePlugin.php create mode 100644 lib/Listener/SabrePluginAddListener.php diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index d4f0b775b..6dbfc45b7 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -11,12 +11,14 @@ use OCA\Calendar\Dashboard\CalendarWidget; use OCA\Calendar\Events\BeforeAppointmentBookedEvent; use OCA\Calendar\Listener\AppointmentBookedListener; use OCA\Calendar\Listener\CalendarReferenceListener; +use OCA\Calendar\Listener\SabrePluginAddListener; use OCA\Calendar\Listener\UserDeletedListener; use OCA\Calendar\Middleware\InvitationMiddleware; use OCA\Calendar\Notification\Notifier; use OCA\Calendar\Profile\AppointmentsAction; use OCA\Calendar\Reference\ReferenceProvider; use OCA\DAV\Connector\Sabre\Principal; +use OCA\DAV\Events\SabrePluginAddEvent; use OCP\AppFramework\App; use OCP\AppFramework\Bootstrap\IBootContext; use OCP\AppFramework\Bootstrap\IBootstrap; @@ -116,6 +118,7 @@ class Application extends App implements IBootstrap { $context->registerEventListener(BeforeAppointmentBookedEvent::class, AppointmentBookedListener::class); $context->registerEventListener(UserDeletedEvent::class, UserDeletedListener::class); $context->registerEventListener(RenderReferenceEvent::class, CalendarReferenceListener::class); + $context->registerEventListener(SabrePluginAddEvent::class, SabrePluginAddListener::class); $context->registerNotifierService(Notifier::class); } diff --git a/lib/Dav/BeforeCreateFilePlugin.php b/lib/Dav/BeforeCreateFilePlugin.php new file mode 100644 index 000000000..8e2055800 --- /dev/null +++ b/lib/Dav/BeforeCreateFilePlugin.php @@ -0,0 +1,225 @@ +server = $server; + $server->on('beforeCreateFile', [$this, 'beforeCreateFile']); + $server->on('beforeWriteContent', [$this, 'beforeWriteContent']); + $this->logger->info('BeforeCreateFilePlugin initialized on Sabre server', ['app' => 'calendar']); + } + + + + /** + * Assign a Nextcloud Talk room URL as the event location + * + * @param string $uri target file URI + * @param resource $data data + * @param INode $parent Sabre Node + * @param bool $modified modified + * @return + */ + + public function beforeCreateFile($uri, &$data, INode $parent, &$modified) { + $this->logger->debug('beforeCreateFile fired', ['app' => 'calendar', 'uri' => $uri]); + if (is_resource($data)) { + $data = stream_get_contents($data); + } + // Parse the data as a vCalendar object using Sabre\VObject. + try { + $vCalendar = Reader::read($data); + } catch (\Exception $e) { + // The data is not a valid vCalendar object. + $this->logger->debug('beforeCreateFile: payload is not valid vCalendar, skipping', [ + 'app' => 'calendar', + 'uri' => $uri, + 'exception' => $e, + ]); + return; + } + $this->processEventData($data, $vCalendar); + + } + + /** + * Assign a Nextcloud Talk room URL as the event location + * + * @param string $uri target file URI + * @param INode $parent Sabre Node + * @param resource $data data + * @param bool $modified modified + * @return + */ + + public function beforeWriteContent($uri, INode $parent, &$data, &$modified) { + $this->logger->debug('beforeWriteContent fired', ['app' => 'calendar', 'uri' => $uri]); + if (is_resource($data)) { + $data = stream_get_contents($data); + } + // Parse the data as a vCalendar object using Sabre\VObject. + try { + $vCalendar = Reader::read($data); + } catch (\Exception $e) { + // The data is not a valid vCalendar object. + $this->logger->debug('beforeWriteContent: payload is not valid vCalendar, skipping', [ + 'app' => 'calendar', + 'uri' => $uri, + 'exception' => $e, + ]); + return; + } + $this->processEventData($data, $vCalendar); + } + + /** + * Process event data and, for an event that has attendees but no location, + * create a Nextcloud Talk room and inject its URL as the LOCATION. + * + * @param string $data Raw iCalendar payload, mutated in place + * @param Document $vCalendar Parsed vCalendar, used to read the event title + */ + private function processEventData(string &$data, Document $vCalendar): void { + $hasAttendee = strpos($data, 'ATTENDEE;') !== false; + $hasLocation = strpos($data, 'LOCATION:') !== false; + $this->logger->debug('processEventData: inspecting vCalendar payload', [ + 'app' => 'calendar', + 'hasAttendee' => $hasAttendee, + 'hasLocation' => $hasLocation, + ]); + + if (!$hasAttendee || $hasLocation) { + $this->logger->debug('processEventData: skipping, event has no attendees or already has a location', [ + 'app' => 'calendar', + 'hasAttendee' => $hasAttendee, + 'hasLocation' => $hasLocation, + ]); + return; + } + + $meetingURL = $this->createTalkRoom($vCalendar); + if ($meetingURL === null) { + $this->logger->debug('processEventData: no Talk room URL, leaving event unchanged', ['app' => 'calendar']); + return; + } + + // Insert LOCATION on the first VEVENT only + $data = preg_replace( + '/BEGIN:VEVENT/', + "BEGIN:VEVENT\r\nLOCATION:$meetingURL", + $data, + 1, + ); + $this->logger->info('processEventData: injected Talk room LOCATION into event', [ + 'app' => 'calendar', + 'url' => $meetingURL, + ]); + } + + /** + * Create a public Talk room for this event and return its absolute URL. + * Returns null if Talk is unavailable, no user is logged in, or room + * creation fails for any reason. + */ + private function createTalkRoom(Document $vCalendar): ?string { + $spreedEnabled = $this->appManager->isEnabledForUser('spreed'); + $roomServiceExists = class_exists(RoomService::class); + if (!$spreedEnabled || !$roomServiceExists) { + $this->logger->info('createTalkRoom: Talk unavailable, skipping', [ + 'app' => 'calendar', + 'spreedEnabled' => $spreedEnabled, + 'roomServiceExists' => $roomServiceExists, + ]); + return null; + } + + $user = $this->userSession->getUser(); + if (!$user instanceof IUser) { + $this->logger->warning('createTalkRoom: no current user in session, skipping', ['app' => 'calendar']); + return null; + } + + $title = 'Talk conversation for event'; + if (isset($vCalendar->VEVENT, $vCalendar->VEVENT->SUMMARY)) { + $summary = trim((string)$vCalendar->VEVENT->SUMMARY); + if ($summary !== '') { + $title = $summary; + } + } + $this->logger->debug('createTalkRoom: creating public Talk room', [ + 'app' => 'calendar', + 'user' => $user->getUID(), + 'title' => $title, + ]); + + try { + /** @var RoomService $roomService */ + $roomService = $this->container->get(RoomService::class); + // Talk room type 3 is a public conversation, matching the + // frontend's createTalkRoom in src/services/talkService.js. + $room = $roomService->createConversation( + 3, + $title, + $user, + 'event', + md5((string)microtime(true)), + ); + + $url = $this->urlGenerator->linkToRouteAbsolute( + 'spreed.Page.showCall', + ['token' => $room->getToken()], + ); + $this->logger->info('createTalkRoom: Talk room created', [ + 'app' => 'calendar', + 'token' => $room->getToken(), + 'url' => $url, + ]); + return $url; + } catch (\Throwable $e) { + $this->logger->warning( + 'Could not create Talk room for CalDAV event: ' . $e->getMessage(), + ['app' => 'calendar', 'exception' => $e], + ); + return null; + } + } +} diff --git a/lib/Listener/SabrePluginAddListener.php b/lib/Listener/SabrePluginAddListener.php new file mode 100644 index 000000000..711c447df --- /dev/null +++ b/lib/Listener/SabrePluginAddListener.php @@ -0,0 +1,38 @@ + + */ +class SabrePluginAddListener implements IEventListener { + public function __construct( + private BeforeCreateFilePlugin $beforeCreateFilePlugin, + private LoggerInterface $logger, + ) { + } + + #[\Override] + public function handle(Event $event): void { + if (!$event instanceof SabrePluginAddEvent) { + return; + } + + $this->logger->info('SabrePluginAddEvent fired, attaching BeforeCreateFilePlugin to Sabre server', ['app' => Application::APP_ID]); + $event->getServer()->addPlugin($this->beforeCreateFilePlugin); + } +} -- GitLab From d8e3c149dbd6b4d1aecc26b41888c96e5d0aaa3f Mon Sep 17 00:00:00 2001 From: Fahim Salam Chowdhury Date: Wed, 6 May 2026 14:54:27 +0600 Subject: [PATCH 2/2] chore: update conversation type --- lib/Dav/BeforeCreateFilePlugin.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Dav/BeforeCreateFilePlugin.php b/lib/Dav/BeforeCreateFilePlugin.php index 8e2055800..43c69ea53 100644 --- a/lib/Dav/BeforeCreateFilePlugin.php +++ b/lib/Dav/BeforeCreateFilePlugin.php @@ -194,10 +194,10 @@ class BeforeCreateFilePlugin extends ServerPlugin { try { /** @var RoomService $roomService */ $roomService = $this->container->get(RoomService::class); - // Talk room type 3 is a public conversation, matching the + // Talk room type 7 is an one time conversation, matching the // frontend's createTalkRoom in src/services/talkService.js. $room = $roomService->createConversation( - 3, + 7, $title, $user, 'event', -- GitLab