Add booking notifications

Signed-off-by: Anna Larch <anna@nextcloud.com>
This commit is contained in:
Anna Larch 2023-02-16 13:19:45 +01:00
parent acabbe15a3
commit aefd8fc07e
6 changed files with 645 additions and 10 deletions

View File

@ -25,6 +25,7 @@ namespace OCA\Calendar\AppInfo;
use OCA\Calendar\Dashboard\CalendarWidget;
use OCA\Calendar\Listener\UserDeletedListener;
use OCA\Calendar\Notification\Notifier;
use OCA\Calendar\Profile\AppointmentsAction;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
@ -56,6 +57,8 @@ class Application extends App implements IBootstrap {
}
$context->registerEventListener(UserDeletedEvent::class, UserDeletedListener::class);
$context->registerNotifierService(Notifier::class);
}
/**

View File

@ -0,0 +1,121 @@
<?php
declare(strict_types=1);
/**
* Calendar App
*
* @copyright 2023 Anna Larch <anna.larch@gmx.net>
*
* @author Anna Larch <anna.larch@gmx.net>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library 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 library. If not, see <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Calendar\Notification;
use OCA\Calendar\AppInfo\Application;
use OCP\IURLGenerator;
use OCP\L10N\IFactory;
use OCP\Notification\INotification;
use OCP\Notification\INotifier;
class Notifier implements INotifier {
private IFactory $factory;
private IURLGenerator $url;
public function __construct(IFactory $factory,
IURLGenerator $url) {
$this->factory = $factory;
$this->url = $url;
}
public function getID(): string {
return Application::APP_ID;
}
/**
* Human-readable name describing the notifier
* @return string
*/
public function getName(): string {
return $this->factory->get(Application::APP_ID)->t('Calendar');
}
public function prepare(INotification $notification, string $languageCode): INotification {
if ($notification->getApp() !== Application::APP_ID) {
// Not my app => throw
throw new \InvalidArgumentException();
}
// Read the language from the notification
$l = $this->factory->get(Application::APP_ID, $languageCode);
switch ($notification->getSubject()) {
// Deal with known subjects
case 'booking_accepted':
$parameters = $notification->getSubjectParameters();
$notification->setRichSubject($l->t('New booking {booking}'), [
'booking' => [
'id' => $parameters['id'],
'type' => $parameters['type'],
'name' => $parameters['name'],
'link' => $this->url->linkToRouteAbsolute('calendar.view.index')
]
]);
$placeholders = $replacements = [];
foreach ($notification->getRichSubjectParameters() as $placeholder => $parameter) {
$placeholders[] = '{' . $placeholder . '}';
$replacements[] = $parameter[$placeholder];
}
$notification->setParsedSubject(str_replace($placeholders, $replacements, $notification->getRichSubject()));
$messageParameters = $notification->getMessageParameters();
$notification->setRichMessage($l->t('{display_name} ({email}) booked the appointment "{config_display_name}" on {date_time}.'), [
'display_name' => [
'type' => 'highlight',
'id' => $messageParameters['id'],
'name' => $messageParameters['display_name'],
],
'email' => [
'type' => 'highlight',
'id' => $messageParameters['id'],
'name' => $messageParameters['email'],
],
'date_time' => [
'type' => 'highlight',
'id' => $messageParameters['id'],
'name' => $messageParameters['date_time'],
],
'config_display_name' => [
'type' => 'highlight',
'id' => $messageParameters['id'],
'name' => $messageParameters['config_display_name'],
]
]);
foreach ($notification->getRichMessageParameters() as $placeholder => $parameter) {
$placeholders[] = '{' . $placeholder . '}';
$replacements[] = $parameter[$placeholder];
}
$notification->setParsedMessage(str_replace($placeholders, $replacements, $notification->getRichMessage()));
break;
default:
throw new \InvalidArgumentException();
}
return $notification;
}
}

View File

@ -114,11 +114,20 @@ class BookingService {
$calendar = $this->calendarWriter->write($config, $startObj, $booking->getDisplayName(), $booking->getEmail(), $booking->getDescription());
$booking->setConfirmed(true);
$this->bookingMapper->update($booking);
try {
$this->mailService->sendBookingInformationEmail($booking, $config, $calendar);
$this->mailService->sendOrganizerBookingInformationEmail($booking, $config, $calendar);
} catch (ServiceException $e) {
$this->logger->info('Could not send booking information email after confirmation by user ' . $booking->getEmail(), ['exception' => $e]);
$this->logger->info('Could not send booking emails after confirmation from user ' . $booking->getEmail(), ['exception' => $e]);
}
try {
$this->mailService->sendOrganizerBookingInformationNotification($booking, $config);
} catch (\InvalidArgumentException $e) {
$this->logger->warning('Could not send booking information notification after confirmation by user ' . $booking->getEmail(), ['exception' => $e]);
}
return $booking;
}

View File

@ -27,6 +27,7 @@ declare(strict_types=1);
namespace OCA\Calendar\Service\Appointments;
use Exception;
use OC\Notification\Notification;
use OC\URLGenerator;
use OCA\Calendar\Db\AppointmentConfig;
use OCA\Calendar\Db\Booking;
@ -34,10 +35,12 @@ use OCA\Calendar\Exception\ServiceException;
use OCP\Defaults;
use OCP\IDateTimeFormatter;
use OCP\IL10N;
use OCP\IUser;
use OCP\IUserManager;
use OCP\L10N\IFactory;
use OCP\Mail\IEMailTemplate;
use OCP\Mail\IMailer;
use OCP\Notification\IManager;
use Psr\Log\LoggerInterface;
use function implode;
@ -59,14 +62,17 @@ class MailService {
/** @var IFactory */
private $lFactory;
public function __construct(IMailer $mailer,
IUserManager $userManager,
IL10N $l10n,
Defaults $defaults,
LoggerInterface $logger,
URLGenerator $urlGenerator,
private IManager $notificationManager;
public function __construct(IMailer $mailer,
IUserManager $userManager,
IL10N $l10n,
Defaults $defaults,
LoggerInterface $logger,
URLGenerator $urlGenerator,
IDateTimeFormatter $dateFormatter,
IFactory $lFactory) {
IFactory $lFactory,
IManager $notificationManager) {
$this->userManager = $userManager;
$this->mailer = $mailer;
$this->l10n = $l10n;
@ -75,6 +81,7 @@ class MailService {
$this->urlGenerator = $urlGenerator;
$this->dateFormatter = $dateFormatter;
$this->lFactory = $lFactory;
$this->notificationManager = $notificationManager;
}
/**
@ -173,7 +180,7 @@ class MailService {
$template = $this->mailer->createEMailTemplate('calendar.confirmAppointment');
$template->addHeader();
//Subject
// Subject
$subject = $this->l10n->t('Your appointment "%s" with %s has been accepted', [$config->getName(), $user->getDisplayName()]);
$template->setSubject($subject);
@ -236,7 +243,7 @@ class MailService {
}
if (!empty($booking->getDescription())) {
$template->addBodyListItem($booking->getDescription(), $l10n->t('Your Comment:'));
$template->addBodyListItem($booking->getDescription(), $l10n->t('Comment:'));
}
}
@ -247,4 +254,97 @@ class MailService {
$instanceName = $this->defaults->getName();
return \OCP\Util::getDefaultEmailAddress('appointments-noreply');
}
public function sendOrganizerBookingInformationEmail(Booking $booking, AppointmentConfig $config, string $calendar) {
/** @var IUser $user */
$user = $this->userManager->get($config->getUserId());
if ($user === null) {
throw new ServiceException('Could not find organizer');
}
$fromName = $user->getDisplayName();
$sys = $this->getSysEmail();
$message = $this->mailer->createMessage()
->setFrom([$sys => $fromName])
->setTo([$user->getEMailAddress() => $booking->getDisplayName()]);
$template = $this->mailer->createEMailTemplate('calendar.confirmOrganizer');
$template->addHeader();
// Subject
$subject = $this->l10n->t('You have a new appointment booking "%s" from %s', [$config->getName(), $booking->getDisplayName()]);
$template->setSubject($subject);
// Heading
$summary = $this->l10n->t('Dear %s, %s (%s) booked an appointment with you.', [$user->getDisplayName(), $booking->getDisplayName(), $booking->getEmail()]);
$template->addHeading($summary);
$template->addBodyListItem($booking->getDisplayName() . ' (' . $booking->getEmail() . ')', 'Appointment with:');
if (!empty($config->getDescription())) {
$template->addBodyListItem($config->getDescription(), 'Description:');
}
// Create Booking overview
$this->addBulletList($template, $this->l10n, $booking, $config->getLocation());
$template->addFooter();
$attachment = $this->mailer->createAttachment($calendar, 'appointment.ics', 'text/calendar');
$message->attach($attachment);
$message->useTemplate($template);
try {
$failed = $this->mailer->send($message);
if (count($failed) > 0) {
$this->logger->warning('Mail delivery failed for some recipients.');
foreach ($failed as $fail) {
$this->logger->debug('Failed to deliver email to ' . $fail);
}
throw new ServiceException('Could not send mail for recipient(s) ' . implode(', ', $failed));
}
} catch (Exception $ex) {
$this->logger->error('Could not send appointment organizer email: ' . $ex->getMessage(), ['exception' => $ex]);
throw new ServiceException('Could not send mail: ' . $ex->getMessage(), $ex->getCode(), $ex);
}
}
public function sendOrganizerBookingInformationNotification(Booking $booking, AppointmentConfig $config) {
$relativeDateTime = $this->dateFormatter->formatDateTimeRelativeDay(
$booking->getStart(),
'long',
'short',
new \DateTimeZone($booking->getTimezone()),
$this->lFactory->get('calendar')
);
/** @var Notification $notification */
$notification = $this->notificationManager->createNotification();
$notification
->setApp('calendar')
->setUser($config->getUserId())
->setObject('booking', (string) $booking->getId())
->setSubject('booking_accepted',
[
'type' => 'highlight',
'id' => $booking->getId(),
'name' => $config->getName(),
'link' => $config->getPrincipalUri()
])
->setDateTime(new \DateTime())
->setMessage('booking_accepted_message',
[
'type' => 'highlight',
'id' => $booking->getId(),
'display_name' => $booking->getDisplayName(),
'config_display_name' => $config->getName(),
'link' => $config->getPrincipalUri(),
'email' => $booking->getEmail(),
'date_time' => $relativeDateTime
]
);
$this->notificationManager->notify($notification);
}
}

View File

@ -0,0 +1,200 @@
<?php
declare(strict_types=1);
/**
*
* Calendar App
*
* @copyright 2023 Anna Larch <anna.larch@gmx.net>
*
* @author Anna Larch <anna.larch@gmx.net>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
* License as published by the Free Software Foundation; either
* version 3 of the License, or any later version.
*
* This library 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 library. If not, see <http://www.gnu.org/licenses/>.
*
*
*/
use OCA\Calendar\Notification\Notifier;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\L10N\IFactory;
use OCP\Notification\INotification;
use PHPUnit\Framework\MockObject\MockObject;
class NotifierTest extends \PHPUnit\Framework\TestCase {
/** @var Notifier */
protected $notifier;
/** @var IFactory|MockObject */
protected $factory;
/** @var IURLGenerator|MockObject */
protected $url;
/** @var IL10N|MockObject */
protected $l;
protected function setUp(): void {
parent::setUp();
$this->l = $this->createMock(IL10N::class);
$this->l->expects($this->any())
->method('t')
->willReturnCallback(function ($string, $args) {
return vsprintf($string, $args);
});
$this->factory = $this->createMock(IFactory::class);
$this->url = $this->createMock(IURLGenerator::class);
$this->factory->expects($this->any())
->method('get')
->willReturn($this->l);
$this->notifier = new Notifier(
$this->factory,
$this->url
);
}
public function testPrepareWrongApp(): void {
$this->expectException(\InvalidArgumentException::class);
/** @var INotification|MockObject $notification */
$notification = $this->createMock(INotification::class);
$notification->expects($this->once())
->method('getApp')
->willReturn('notifications');
$notification->expects($this->never())
->method('getSubject');
$this->notifier->prepare($notification, 'en');
}
public function testPrepareWrongSubject(): void {
$this->expectException(\InvalidArgumentException::class);
/** @var INotification|MockObject $notification */
$notification = $this->createMock(INotification::class);
$notification->expects($this->once())
->method('getApp')
->willReturn('calendar');
$notification->expects($this->once())
->method('getSubject')
->willReturn('wrong subject');
$this->notifier->prepare($notification, 'en');
}
public function testPrepare(): void {
/** @var INotification|MockObject $notification */
$notification = $this->createMock(INotification::class);
$notification->setApp('calendar')
->setUser('test')
->setObject('booking', '123')
->setSubject('booking_accepted',
[
'type' => 'highlight',
'id' => 123,
'name' => 'Test',
'link' => 'link/to/calendar'
])
->setDateTime(new \DateTime())
->setMessage('booking_accepted_message',
[
'type' => 'highlight',
'id' => 123,
'display_name' => 'Bob',
'config_display_name' => 'Test',
'link' => 'link/to/calendar',
'email' => 'test@test.com',
'date_time' => new \DateTime()
]
);
$parameters = [
'id' => 123,
'type' => 'highlight',
'name' => 'Test',
'link' => 'link/to/calendar'
];
$booking = [
'booking' => $parameters
];
$messageParameters = [
'type' => 'highlight',
'id' => 123,
'display_name' => 'Bob',
'config_display_name' => 'Test',
'link' => 'link/to/calendar',
'email' => 'test@test.com',
'date_time' => new \DateTime()
];
$messageRichData = [
'display_name' => [
'type' => 'highlight',
'id' => $messageParameters['id'],
'name' => $messageParameters['display_name'],
],
'email' => [
'type' => 'highlight',
'id' => $messageParameters['id'],
'name' => $messageParameters['email'],
],
'date_time' => [
'type' => 'highlight',
'id' => $messageParameters['id'],
'name' => $messageParameters['date_time'],
],
'config_display_name' => [
'type' => 'highlight',
'id' => $messageParameters['id'],
'name' => $messageParameters['config_display_name'],
]
];
$this->url->expects($this->once())
->method('linkToRouteAbsolute')
->with('calendar.view.index')
->willReturn('link/to/calendar');
$notification->expects($this->once())
->method('getApp')
->willReturn('calendar');
$notification->expects($this->once())
->method('getSubject')
->willReturn('booking_accepted');
$notification->expects($this->once())
->method('getSubjectParameters')
->willReturn($parameters);
$this->factory->expects($this->once())
->method('get')
->with('calendar', 'de')
->willReturn($this->l);
$notification->expects($this->once())
->method('setRichSubject')
->with('New booking {booking}', $booking);
$notification->expects($this->once())
->method('getRichSubjectParameters');
$notification->expects(self::once())
->method('setParsedSubject');
$notification->expects(self::once())
->method('getMessageParameters')
->willReturn($messageParameters);
$notification->expects($this->once())
->method('setRichMessage')
->with('{display_name} ({email}) booked the appointment "{config_display_name}" on {date_time}.', $messageRichData);
$notification->expects($this->once())
->method('getRichMessageParameters');
$notification->expects(self::once())
->method('setParsedMessage');
$return = $this->notifier->prepare($notification, 'de');
$this->assertEquals($notification, $return);
}
}

View File

@ -42,6 +42,8 @@ use OCP\L10N\IFactory;
use OCP\Mail\IEMailTemplate;
use OCP\Mail\IMailer;
use OCP\Mail\IMessage;
use OCP\Notification\IManager;
use OCP\Notification\INotification;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Log\LoggerInterface;
@ -70,6 +72,9 @@ class MailServiceTest extends TestCase {
/** @var mixed|IFactory|MockObject */
private $lFactory;
/** @var IManager|MockObject */
private $notificationManager;
/** @var MailService */
private $mailService;
@ -88,6 +93,7 @@ class MailServiceTest extends TestCase {
$this->urlGenerator = $this->createMock(URLGenerator::class);
$this->dateFormatter = $this->createMock(IDateTimeFormatter::class);
$this->lFactory = $this->createMock(IFactory::class);
$this->notificationManager = $this->createMock(IManager::class);
$this->mailService = new MailService(
$this->mailer,
$this->userManager,
@ -97,6 +103,7 @@ class MailServiceTest extends TestCase {
$this->urlGenerator,
$this->dateFormatter,
$this->lFactory,
$this->notificationManager,
);
}
@ -487,4 +494,199 @@ class MailServiceTest extends TestCase {
$this->mailService->sendBookingInformationEmail($booking, $config, 'abc');
}
public function testSendOrganizerBookingInformationEmail(): void {
$booking = new Booking();
$booking->setEmail('test@test.com');
$booking->setDisplayName('Test');
$booking->setStart(time());
$booking->setTimezone('Europe/Berlin');
$booking->setDescription('Test');
$config = new AppointmentConfig();
$config->setUserId('test');
$config->setLocation('Test');
$this->userManager->expects(self::once())
->method('get')
->willReturn($this->createConfiguredMock(IUser::class, [
'getEmailAddress' => 'test@test.com',
'getDisplayName' => 'Test Test'
]));
$mailMessage = $this->createMock(IMessage::class);
$this->mailer->expects(self::once())
->method('createMessage')
->willReturn($mailMessage);
$mailMessage->expects(self::once())
->method('setFrom')
->willReturn($mailMessage);
$mailMessage->expects(self::once())
->method('setTo')
->willReturn($mailMessage);
$mailMessage->expects(self::once())
->method('useTemplate')
->willReturn($mailMessage);
$emailTemplate = $this->createMock(IEMailTemplate::class);
$this->mailer->expects(self::once())
->method('createEmailTemplate')
->willReturn($emailTemplate);
$emailTemplate->expects(self::once())
->method('addHeader');
$emailTemplate->expects(self::once())
->method('setSubject');
$emailTemplate->expects(self::once())
->method('addHeading');
$emailTemplate->expects(self::exactly(5))
->method('addBodyListItem');
$emailTemplate->expects(self::once())
->method('addFooter');
$this->mailer->expects(self::once())
->method('createEmailTemplate');
$this->mailer->expects(self::once())
->method('createAttachment');
$this->l10n->expects(self::exactly(6))
->method('t');
$this->lFactory->expects(self::once())
->method('findGenericLanguage')
->willReturn('en');
$this->lFactory->expects(self::once())
->method('get');
$this->dateFormatter->expects(self::once())
->method('formatDateTimeRelativeDay')
->willReturn('Test');
$this->mailer->expects(self::once())
->method('send')
->willReturn([]);
$this->logger->expects(self::never())
->method('warning');
$this->logger->expects(self::never())
->method('debug');
$this->mailService->sendOrganizerBookingInformationEmail($booking, $config, 'abc');
}
public function testSendOrganizerBookingInformationEmailFailed(): void {
$booking = new Booking();
$booking->setEmail('test@test.com');
$booking->setDisplayName('Test');
$booking->setStart(time());
$booking->setTimezone('Europe/Berlin');
$booking->setDescription('Test');
$config = new AppointmentConfig();
$config->setUserId('test');
$config->setLocation('Test');
$this->userManager->expects(self::once())
->method('get')
->willReturn($this->createConfiguredMock(IUser::class, [
'getEmailAddress' => 'test@test.com',
'getDisplayName' => 'Test Test'
]));
$mailMessage = $this->createMock(IMessage::class);
$this->mailer->expects(self::once())
->method('createMessage')
->willReturn($mailMessage);
$mailMessage->expects(self::once())
->method('setFrom')
->willReturn($mailMessage);
$mailMessage->expects(self::once())
->method('setTo')
->willReturn($mailMessage);
$mailMessage->expects(self::once())
->method('useTemplate')
->willReturn($mailMessage);
$emailTemplate = $this->createMock(IEMailTemplate::class);
$this->mailer->expects(self::once())
->method('createEmailTemplate')
->willReturn($emailTemplate);
$emailTemplate->expects(self::once())
->method('addHeader');
$emailTemplate->expects(self::once())
->method('setSubject');
$emailTemplate->expects(self::once())
->method('addHeading');
$emailTemplate->expects(self::exactly(5))
->method('addBodyListItem');
$emailTemplate->expects(self::once())
->method('addFooter');
$this->mailer->expects(self::once())
->method('createEmailTemplate');
$this->mailer->expects(self::once())
->method('createAttachment');
$this->l10n->expects(self::exactly(6))
->method('t');
$this->lFactory->expects(self::once())
->method('findGenericLanguage')
->willReturn('en');
$this->lFactory->expects(self::once())
->method('get');
$this->dateFormatter->expects(self::once())
->method('formatDateTimeRelativeDay')
->willReturn('Test');
$this->mailer->expects(self::once())
->method('send')
->willReturn(['test@test.com']);
$this->logger->expects(self::once())
->method('warning');
$this->logger->expects(self::once())
->method('debug');
$this->expectException(ServiceException::class);
$this->mailService->sendOrganizerBookingInformationEmail($booking, $config, 'abc');
}
public function testSendOrganizerBookingInformationOrganizerNotFound(): void {
$booking = new Booking();
$config = new AppointmentConfig();
$config->setUserId('test');
$this->userManager->expects(self::once())
->method('get')
->willReturn(null);
$this->expectException(ServiceException::class);
$this->mailService->sendOrganizerBookingInformationEmail($booking, $config, 'abc');
}
public function testSendOrganizerBookingNotification(): void {
$booking = new Booking();
$booking->setEmail('test@test.com');
$booking->setDisplayName('Test');
$booking->setStart(time());
$booking->setTimezone('Europe/Berlin');
$booking->setDescription('Test');
$config = new AppointmentConfig();
$config->setUserId('test');
$config->setLocation('Test');
$notification = $this->createMock(INotification::class);
$this->lFactory->expects(self::once())
->method('get')
->willReturn($this->createMock(IL10N::class));
$this->dateFormatter->expects(self::once())
->method('formatDateTimeRelativeDay');
$this->notificationManager->expects(self::once())
->method('createNotification')
->willReturn($notification);
$notification->expects(self::once())
->method('setApp')
->willReturn($notification);
$notification->expects(self::once())
->method('setUser')
->with($config->getUserId())
->willReturn($notification);
$notification->expects(self::once())
->method('setObject')
->willReturn($notification);
$notification->expects(self::once())
->method('setSubject')
->willReturn($notification);
$notification->expects(self::once())
->method('setDateTime')
->willReturn($notification);
$notification->expects(self::once())
->method('setMessage')
->willReturn($notification);
$this->notificationManager->expects(self::once())
->method('notify')
->with($notification);
$this->mailService->sendOrganizerBookingInformationNotification($booking, $config);
}
}