From faa1c72e233f7c2d23ed5ae7442b6b5b05591c53 Mon Sep 17 00:00:00 2001 From: Christoph Wurst Date: Wed, 14 Jun 2023 12:04:20 +0200 Subject: [PATCH] Revert "Revert "Create Talk rooms for appointments"" Signed-off-by: Christoph Wurst --- appinfo/info.xml | 2 +- lib/AppInfo/Application.php | 3 + .../AppointmentConfigController.php | 10 +++- lib/Db/AppointmentConfig.php | 9 ++- lib/Db/AppointmentConfigMapper.php | 6 +- lib/Db/Booking.php | 15 +++++ lib/Events/BeforeAppointmentBookedEvent.php | 54 ++++++++++++++++++ lib/Listener/AppointmentBookedListener.php | 4 +- .../Version4050Date20230614163505.php | 57 +++++++++++++++++++ .../Appointments/AppointmentConfigService.php | 22 +------ .../Appointments/BookingCalendarWriter.php | 8 ++- lib/Service/Appointments/BookingService.php | 25 +++++++- lib/Service/Appointments/MailService.php | 24 ++++++-- src/components/AppointmentConfigModal.vue | 24 +++++++- .../CheckedDurationSelect.vue | 4 +- .../AppointmentConfigModal/TextInput.vue | 5 ++ src/models/appointmentConfig.js | 5 ++ src/store/settings.js | 2 + .../Appointments/BookingServiceTest.php | 7 +++ 19 files changed, 246 insertions(+), 40 deletions(-) create mode 100644 lib/Events/BeforeAppointmentBookedEvent.php create mode 100644 lib/Migration/Version4050Date20230614163505.php diff --git a/appinfo/info.xml b/appinfo/info.xml index 94b0b6ac2..59f077552 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -15,7 +15,7 @@ * ☑️ Tasks! See tasks with a due date directly in the calendar * 🙈 **We’re not reinventing the wheel!** Based on the great [c-dav library](https://github.com/nextcloud/cdav-library), [ical.js](https://github.com/mozilla-comm/ical.js) and [fullcalendar](https://github.com/fullcalendar/fullcalendar) libraries. ]]> - 4.5.0-beta.1 + 4.5.0-beta.2 agpl Anna Larch Nextcloud Groupware Team diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index dd4c5d706..8a21072f3 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -24,6 +24,8 @@ declare(strict_types=1); namespace OCA\Calendar\AppInfo; use OCA\Calendar\Dashboard\CalendarWidget; +use OCA\Calendar\Events\BeforeAppointmentBookedEvent; +use OCA\Calendar\Listener\AppointmentBookedListener; use OCA\Calendar\Listener\UserDeletedListener; use OCA\Calendar\Notification\Notifier; use OCA\Calendar\Profile\AppointmentsAction; @@ -56,6 +58,7 @@ class Application extends App implements IBootstrap { $context->registerProfileLinkAction(AppointmentsAction::class); } + $context->registerEventListener(BeforeAppointmentBookedEvent::class, AppointmentBookedListener::class); $context->registerEventListener(UserDeletedEvent::class, UserDeletedListener::class); $context->registerNotifierService(Notifier::class); diff --git a/lib/Controller/AppointmentConfigController.php b/lib/Controller/AppointmentConfigController.php index e400df15f..9a566d27b 100644 --- a/lib/Controller/AppointmentConfigController.php +++ b/lib/Controller/AppointmentConfigController.php @@ -164,7 +164,8 @@ class AppointmentConfigController extends Controller { ?array $calendarFreeBusyUris = null, ?int $start = null, ?int $end = null, - ?int $futureLimit = null): JsonResponse { + ?int $futureLimit = null, + ?bool $createTalkRoom = false): JsonResponse { if ($this->userId === null) { return JsonResponse::fail(); } @@ -192,7 +193,8 @@ class AppointmentConfigController extends Controller { $calendarFreeBusyUris, $start, $end, - $futureLimit + $futureLimit, + $createTalkRoom ); return JsonResponse::success($appointmentConfig); } catch (ServiceException $e) { @@ -240,7 +242,8 @@ class AppointmentConfigController extends Controller { ?array $calendarFreeBusyUris = null, ?int $start = null, ?int $end = null, - ?int $futureLimit = null): JsonResponse { + ?int $futureLimit = null, + ?bool $createTalkRoom = false): JsonResponse { if ($this->userId === null) { return JsonResponse::fail(null, Http::STATUS_NOT_FOUND); } @@ -274,6 +277,7 @@ class AppointmentConfigController extends Controller { $appointmentConfig->setStart($start); $appointmentConfig->setEnd($end); $appointmentConfig->setFutureLimit($futureLimit); + $appointmentConfig->setCreateTalkRoom($createTalkRoom === true); try { $appointmentConfig = $this->appointmentConfigService->update($appointmentConfig); diff --git a/lib/Db/AppointmentConfig.php b/lib/Db/AppointmentConfig.php index c2251c2ba..952ba14aa 100644 --- a/lib/Db/AppointmentConfig.php +++ b/lib/Db/AppointmentConfig.php @@ -71,6 +71,8 @@ use function json_encode; * @method void setDailyMax(?int $max) * @method int|null getFutureLimit() * @method void setFutureLimit(?int $limit) + * @method int|null getCreateTalkRoom() + * @method void setCreateTalkRoom(bool $create) */ class AppointmentConfig extends Entity implements JsonSerializable { /** @var string */ @@ -127,6 +129,9 @@ class AppointmentConfig extends Entity implements JsonSerializable { /** @var int|null */ protected $futureLimit; + /** @var bool */ + protected $createTalkRoom; + /** @var string */ public const VISIBILITY_PUBLIC = 'PUBLIC'; @@ -143,6 +148,7 @@ class AppointmentConfig extends Entity implements JsonSerializable { $this->addType('timeBeforeNextSlot', 'int'); $this->addType('dailyMax', 'int'); $this->addType('futureLimit', 'int'); + $this->addType('createTalkRoom', 'boolean'); } /** @@ -205,7 +211,8 @@ class AppointmentConfig extends Entity implements JsonSerializable { 'totalLength' => $this->getTotalLength(), 'timeBeforeNextSlot' => $this->getTimeBeforeNextSlot(), 'dailyMax' => $this->getDailyMax(), - 'futureLimit' => $this->getFutureLimit() + 'futureLimit' => $this->getFutureLimit(), + 'createTalkRoom' => $this->getCreateTalkRoom(), ]; } } diff --git a/lib/Db/AppointmentConfigMapper.php b/lib/Db/AppointmentConfigMapper.php index 709202a81..73245671d 100644 --- a/lib/Db/AppointmentConfigMapper.php +++ b/lib/Db/AppointmentConfigMapper.php @@ -48,7 +48,7 @@ class AppointmentConfigMapper extends QBMapper { */ public function findByIdForUser(int $id, string $userId) : AppointmentConfig { $qb = $this->db->getQueryBuilder(); - $qb->select('id', 'token', 'name', 'description', 'location', 'visibility', 'user_id', 'target_calendar_uri', 'calendar_freebusy_uris', 'availability', 'start', 'end', 'length', 'increment', 'preparation_duration', 'followup_duration', 'time_before_next_slot', 'daily_max', 'future_limit') + $qb->select('*') ->from($this->getTableName()) ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT)) ->andWhere($qb->expr()->eq('user_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR), IQueryBuilder::PARAM_STR)); @@ -64,7 +64,7 @@ class AppointmentConfigMapper extends QBMapper { */ public function findById(int $id) : AppointmentConfig { $qb = $this->db->getQueryBuilder(); - $qb->select('id', 'token', 'name', 'description', 'location', 'visibility', 'user_id', 'target_calendar_uri', 'calendar_freebusy_uris', 'availability', 'start', 'end', 'length', 'increment', 'preparation_duration', 'followup_duration', 'time_before_next_slot', 'daily_max', 'future_limit') + $qb->select('*') ->from($this->getTableName()) ->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT), IQueryBuilder::PARAM_INT)); return $this->findEntity($qb); @@ -92,7 +92,7 @@ class AppointmentConfigMapper extends QBMapper { */ public function findAllForUser(string $userId, string $visibility = null): array { $qb = $this->db->getQueryBuilder(); - $qb->select('id', 'token', 'name', 'description', 'location', 'visibility', 'user_id', 'target_calendar_uri', 'calendar_freebusy_uris', 'availability', 'start', 'end', 'length', 'increment', 'preparation_duration', 'followup_duration', 'time_before_next_slot', 'daily_max', 'future_limit') + $qb->select('*') ->from($this->getTableName()) ->where($qb->expr()->eq('user_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR), IQueryBuilder::PARAM_STR)); if ($visibility !== null) { diff --git a/lib/Db/Booking.php b/lib/Db/Booking.php index 3cfec5b5c..a7b7f1530 100644 --- a/lib/Db/Booking.php +++ b/lib/Db/Booking.php @@ -85,6 +85,13 @@ class Booking extends Entity implements JsonSerializable { /** @var bool */ protected $confirmed; + /** + * Transient talk URL + * + * @var string|null + */ + private $talkUrl; + public function __construct() { $this->addType('id', 'integer'); $this->addType('apptConfigId', 'integer'); @@ -110,4 +117,12 @@ class Booking extends Entity implements JsonSerializable { 'confirmed' => $this->isConfirmed(), ]; } + + public function getTalkUrl(): ?string { + return $this->talkUrl; + } + + public function setTalkUrl(string $talkUrl): void { + $this->talkUrl = $talkUrl; + } } diff --git a/lib/Events/BeforeAppointmentBookedEvent.php b/lib/Events/BeforeAppointmentBookedEvent.php new file mode 100644 index 000000000..e5df78bfd --- /dev/null +++ b/lib/Events/BeforeAppointmentBookedEvent.php @@ -0,0 +1,54 @@ + + * + * @author 2022 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\Calendar\Events; + +use OCA\Calendar\Db\AppointmentConfig; +use OCA\Calendar\Db\Booking; +use OCP\EventDispatcher\Event; + +class BeforeAppointmentBookedEvent extends Event { + + /** @var Booking */ + private $booking; + /** @var AppointmentConfig */ + + private $config; + + public function __construct(Booking $booking, AppointmentConfig $config) { + parent::__construct(); + + $this->booking = $booking; + $this->config = $config; + } + + public function getBooking(): Booking { + return $this->booking; + } + + public function getConfig(): AppointmentConfig { + return $this->config; + } +} diff --git a/lib/Listener/AppointmentBookedListener.php b/lib/Listener/AppointmentBookedListener.php index 9c4a3dce4..0a3457c16 100644 --- a/lib/Listener/AppointmentBookedListener.php +++ b/lib/Listener/AppointmentBookedListener.php @@ -25,7 +25,7 @@ declare(strict_types=1); namespace OCA\Calendar\Listener; -use OCA\Calendar\Events\AppointmentBookedEvent; +use OCA\Calendar\Events\BeforeAppointmentBookedEvent; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; use OCP\IL10N; @@ -57,7 +57,7 @@ class AppointmentBookedListener implements IEventListener { } public function handle(Event $event): void { - if (!($event instanceof AppointmentBookedEvent)) { + if (!($event instanceof BeforeAppointmentBookedEvent)) { // Don't care return; } diff --git a/lib/Migration/Version4050Date20230614163505.php b/lib/Migration/Version4050Date20230614163505.php new file mode 100644 index 000000000..4d509c95d --- /dev/null +++ b/lib/Migration/Version4050Date20230614163505.php @@ -0,0 +1,57 @@ + + * + * @author 2022 Christoph Wurst + * + * @license GNU AGPL version 3 or any later version + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\Calendar\Migration; + +use Closure; +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +class Version4050Date20230614163505 extends SimpleMigrationStep { + + /** + * @param IOutput $output + * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + * @return null|ISchemaWrapper + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + + $table = $schema->getTable('calendar_appt_configs'); + if (!$table->hasColumn('create_talk_room')) { + $table->addColumn('create_talk_room', Types::BOOLEAN, [ + 'notnull' => false, + 'default' => false, + ]); + } + + return $schema; + } + +} diff --git a/lib/Service/Appointments/AppointmentConfigService.php b/lib/Service/Appointments/AppointmentConfigService.php index 2d831a7f1..a45b3cb23 100644 --- a/lib/Service/Appointments/AppointmentConfigService.php +++ b/lib/Service/Appointments/AppointmentConfigService.php @@ -142,24 +142,6 @@ class AppointmentConfigService { } /** - * @param string $name - * @param string $description - * @param string|null $location - * @param string $visibility - * @param string $userId - * @param string $targetCalendarUri - * @param array $availability - * @param int $length - * @param int $increment - * @param int $preparationDuration - * @param int $followupDuration - * @param int $buffer - * @param int|null $dailyMax - * @param string[] $calendarFreeBusyUris - * @param int|null $start - * @param int|null $end - * @param int|null $futureLimit - * @return AppointmentConfig * @throws ServiceException */ public function create(string $name, @@ -178,7 +160,8 @@ class AppointmentConfigService { ?array $calendarFreeBusyUris = [], ?int $start = null, ?int $end = null, - ?int $futureLimit = null): AppointmentConfig { + ?int $futureLimit = null, + ?bool $createTalkRoom = false): AppointmentConfig { try { $appointmentConfig = new AppointmentConfig(); $appointmentConfig->setToken($this->random->generate(12, ISecureRandom::CHAR_HUMAN_READABLE)); @@ -199,6 +182,7 @@ class AppointmentConfigService { $appointmentConfig->setStart($start); $appointmentConfig->setEnd($end); $appointmentConfig->setFutureLimit($futureLimit); + $appointmentConfig->setCreateTalkRoom($createTalkRoom === true); return $this->mapper->insert($appointmentConfig); } catch (DbException $e) { diff --git a/lib/Service/Appointments/BookingCalendarWriter.php b/lib/Service/Appointments/BookingCalendarWriter.php index 40dc2032f..274ecbe8b 100644 --- a/lib/Service/Appointments/BookingCalendarWriter.php +++ b/lib/Service/Appointments/BookingCalendarWriter.php @@ -93,7 +93,12 @@ class BookingCalendarWriter { * @return string * @throws RuntimeException */ - public function write(AppointmentConfig $config, DateTimeImmutable $start, string $displayName, string $email, ?string $description = null) : string { + public function write(AppointmentConfig $config, + DateTimeImmutable $start, + string $displayName, + string $email, + ?string $description = null, + ?string $location = null) : string { $calendar = current($this->manager->getCalendarsForPrincipal($config->getPrincipalUri(), [$config->getTargetCalendarUri()])); if (!($calendar instanceof ICreateFromString)) { throw new RuntimeException('Could not find a public writable calendar for this principal'); @@ -166,6 +171,7 @@ class BookingCalendarWriter { $vcalendar->VEVENT->add($alarm); } + if ($config->getLocation() !== null) { $vcalendar->VEVENT->add('LOCATION', $config->getLocation()); } diff --git a/lib/Service/Appointments/BookingService.php b/lib/Service/Appointments/BookingService.php index ed1a3e9b9..7af7781df 100644 --- a/lib/Service/Appointments/BookingService.php +++ b/lib/Service/Appointments/BookingService.php @@ -32,6 +32,7 @@ use InvalidArgumentException; use OCA\Calendar\Db\AppointmentConfig; use OCA\Calendar\Db\Booking; use OCA\Calendar\Db\BookingMapper; +use OCA\Calendar\Events\BeforeAppointmentBookedEvent; use OCA\Calendar\Exception\ClientException; use OCA\Calendar\Exception\NoSlotFoundException; use OCA\Calendar\Exception\ServiceException; @@ -39,6 +40,7 @@ use OCP\AppFramework\Db\DoesNotExistException; use OCP\AppFramework\Http; use OCP\DB\Exception; use OCP\DB\Exception as DbException; +use OCP\EventDispatcher\IEventDispatcher; use OCP\IUser; use OCP\Security\ISecureRandom; use Psr\Log\LoggerInterface; @@ -72,6 +74,9 @@ class BookingService { /** @var MailService */ private $mailService; + /** @var IEventDispatcher */ + private $eventDispatcher; + /** @var LoggerInterface */ private $logger; @@ -83,6 +88,7 @@ class BookingService { BookingCalendarWriter $calendarWriter, ISecureRandom $random, MailService $mailService, + IEventDispatcher $eventDispatcher, LoggerInterface $logger) { $this->availabilityGenerator = $availabilityGenerator; $this->extrapolator = $extrapolator; @@ -92,6 +98,7 @@ class BookingService { $this->bookingMapper = $bookingMapper; $this->random = $random; $this->mailService = $mailService; + $this->eventDispatcher = $eventDispatcher; $this->logger = $logger; } @@ -111,10 +118,26 @@ class BookingService { throw new ClientException('Could not make sense of booking times'); } - $calendar = $this->calendarWriter->write($config, $startObj, $booking->getDisplayName(), $booking->getEmail(), $booking->getDescription()); + // TODO: inject broker here and remove indirection + $this->eventDispatcher->dispatchTyped( + new BeforeAppointmentBookedEvent( + $booking, + $config, + ) + ); + + $calendar = $this->calendarWriter->write( + $config, + $startObj, + $booking->getDisplayName(), + $booking->getEmail(), + $booking->getDescription(), + $config->getCreateTalkRoom() ? $booking->getTalkUrl() : $config->getLocation(), + ); $booking->setConfirmed(true); $this->bookingMapper->update($booking); + try { $this->mailService->sendBookingInformationEmail($booking, $config, $calendar); $this->mailService->sendOrganizerBookingInformationEmail($booking, $config, $calendar); diff --git a/lib/Service/Appointments/MailService.php b/lib/Service/Appointments/MailService.php index 90287ff27..df136d3cf 100644 --- a/lib/Service/Appointments/MailService.php +++ b/lib/Service/Appointments/MailService.php @@ -41,6 +41,7 @@ use OCP\Mail\IEMailTemplate; use OCP\Mail\IMailer; use OCP\Notification\IManager; use Psr\Log\LoggerInterface; +use function htmlspecialchars; use function implode; class MailService { @@ -128,7 +129,7 @@ class MailService { } // Create Booking overview - $this->addBulletList($template, $this->l10n, $booking, $config->getLocation()); + $this->addBulletList($template, $this->l10n, $booking, $config); $bodyText = $this->l10n->t('This confirmation link expires in %s hours.', [(BookingService::EXPIRY / 3600)]); $template->addBodyText($bodyText); @@ -198,7 +199,7 @@ class MailService { } // Create Booking overview - $this->addBulletList($template, $this->l10n, $booking, $config->getLocation()); + $this->addBulletList($template, $this->l10n, $booking, $config); $bodyText = $this->l10n->t('If you wish to cancel the appointment after all, please contact your organizer by replying to this email or by visiting their profile page.'); $template->addBodyText($bodyText); @@ -228,7 +229,7 @@ class MailService { private function addBulletList(IEMailTemplate $template, IL10N $l10n, Booking $booking, - ?string $location = null):void { + AppointmentConfig $config):void { $template->addBodyListItem($booking->getDisplayName(), $l10n->t('Appointment for:')); $l = $this->lFactory->findGenericLanguage(); @@ -242,8 +243,19 @@ class MailService { $template->addBodyListItem($relativeDateTime, $l10n->t('Date:')); - if (!empty($location)) { - $template->addBodyListItem($location, $l10n->t('Where:')); + if (!$booking->isConfirmed() && $config->getCreateTalkRoom()) { + $template->addBodyListItem($l10n->t('You will receive a link with the confirmation email'), $l10n->t('Where:')); + } elseif (!$booking->isConfirmed() && !empty($config->getLocation())) { + $template->addBodyListItem($config->getLocation(), $l10n->t('Where:')); + } elseif ($booking->isConfirmed() && $booking->getTalkUrl() !== null) { + $template->addBodyListItem( + '' . $booking->getTalkUrl() . '', + $l10n->t('Where:'), + '', + $booking->getTalkUrl(), + ); + } elseif ($booking->isConfirmed() && !empty($config->getLocation())) { + $template->addBodyListItem($config->getLocation(), $l10n->t('Where:')); } if (!empty($booking->getDescription())) { @@ -296,7 +308,7 @@ class MailService { } // Create Booking overview - $this->addBulletList($template, $this->l10n, $booking, $config->getLocation()); + $this->addBulletList($template, $this->l10n, $booking, $config); $template->addFooter(); $attachment = $this->mailer->createAttachment($calendar, 'appointment.ics', 'text/calendar'); diff --git a/src/components/AppointmentConfigModal.vue b/src/components/AppointmentConfigModal.vue index 2c9021a96..a461c6bd1 100644 --- a/src/components/AppointmentConfigModal.vue +++ b/src/components/AppointmentConfigModal.vue @@ -38,7 +38,14 @@ :value.sync="editing.name" /> + :value.sync="editing.location" + :disabled="isTalkEnabled && editing.createTalkRoom" /> +
+ + {{ t('calendar', 'Create a Talk room') }} + + {{ t('calendar', 'A unique link will be generated for every booked appointment and sent via the confirmation email') }} +