migrate therinaldos.com data
Build & Deploy to DigitalOcean Space / build (push) Failing after 2m38s

This commit is contained in:
2024-05-05 15:50:45 -04:00
commit ef1ff240d4
23182 changed files with 3801898 additions and 0 deletions
+101
View File
@@ -0,0 +1,101 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Notifications;
use OCA\Notifications\Exceptions\NotificationNotFoundException;
use OCP\Notification\IDeferrableApp;
use OCP\Notification\INotification;
use Symfony\Component\Console\Output\OutputInterface;
class App implements IDeferrableApp {
/** @var Handler */
protected $handler;
/** @var Push */
protected $push;
public function __construct(Handler $handler,
Push $push) {
$this->handler = $handler;
$this->push = $push;
}
public function setOutput(OutputInterface $output): void {
$this->push->setOutput($output);
}
/**
* @param INotification $notification
* @throws \InvalidArgumentException When the notification is not valid
* @since 8.2.0
*/
public function notify(INotification $notification): void {
$notificationId = $this->handler->add($notification);
try {
$this->push->pushToDevice($notificationId, $notification);
} catch (NotificationNotFoundException $e) {
throw new \InvalidArgumentException('Error while preparing push notification');
}
}
/**
* @param INotification $notification
* @return int
* @since 8.2.0
*/
public function getCount(INotification $notification): int {
return $this->handler->count($notification);
}
/**
* @param INotification $notification
* @since 8.2.0
*/
public function markProcessed(INotification $notification): void {
$deleted = $this->handler->delete($notification);
$isAlreadyDeferring = $this->push->isDeferring();
if (!$isAlreadyDeferring) {
$this->push->deferPayloads();
}
foreach ($deleted as $user => $notifications) {
foreach ($notifications as $data) {
$this->push->pushDeleteToDevice((string) $user, [$data['id']], $data['app']);
}
}
if (!$isAlreadyDeferring) {
$this->push->flushPayloads();
}
}
public function defer(): void {
$this->push->deferPayloads();
}
public function flush(): void {
$this->push->flushPayloads();
}
}
@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023, Joas Schilling <coding@schilljs.com>
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Notifications\AppInfo;
use OC\Authentication\Token\IProvider;
use OCA\Notifications\App;
use OCA\Notifications\Capabilities;
use OCA\Notifications\Listener\BeforeTemplateRenderedListener;
use OCA\Notifications\Listener\PostLoginListener;
use OCA\Notifications\Listener\UserCreatedListener;
use OCA\Notifications\Listener\UserDeletedListener;
use OCA\Notifications\Notifier\AdminNotifications;
use OCA\Notifications\Settings\SetupWarningOnRateLimitReached;
use OCP\AppFramework\Bootstrap\IBootContext;
use OCP\AppFramework\Bootstrap\IBootstrap;
use OCP\AppFramework\Bootstrap\IRegistrationContext;
use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent;
use OCP\AppFramework\IAppContainer;
use OCP\Notification\IManager;
use OCP\User\Events\PostLoginEvent;
use OCP\User\Events\UserCreatedEvent;
use OCP\User\Events\UserDeletedEvent;
class Application extends \OCP\AppFramework\App implements IBootstrap {
public const APP_ID = 'notifications';
public function __construct() {
parent::__construct(self::APP_ID);
}
public function register(IRegistrationContext $context): void {
$context->registerCapability(Capabilities::class);
$context->registerService(IProvider::class, function (IAppContainer $c) {
return $c->getServer()->get(IProvider::class);
});
$context->registerSetupCheck(SetupWarningOnRateLimitReached::class);
$context->registerNotifierService(AdminNotifications::class);
$context->registerEventListener(UserDeletedEvent::class, UserDeletedListener::class);
$context->registerEventListener(BeforeTemplateRenderedEvent::class, BeforeTemplateRenderedListener::class);
$context->registerEventListener(UserCreatedEvent::class, UserCreatedListener::class);
$context->registerEventListener(PostLoginEvent::class, PostLoginListener::class);
}
public function boot(IBootContext $context): void {
$context->injectFn(\Closure::fromCallable([$this, 'registerAppAndNotifier']));
}
public function registerAppAndNotifier(IManager $notificationManager): void {
// notification app
$notificationManager->registerApp(App::class);
}
}
@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2021, Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license AGPL-3.0-or-later
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Notifications\BackgroundJob;
use OCA\Notifications\Model\Settings;
use OCA\Notifications\Model\SettingsMapper;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob;
use OCP\IDBConnection;
use OCP\IUser;
use OCP\IUserManager;
class GenerateUserSettings extends TimedJob {
/** @var IDBConnection */
private $connection;
/** @var IUserManager */
private $userManager;
/** @var SettingsMapper */
private $settingsMapper;
public function __construct(
ITimeFactory $time,
IDBConnection $connection,
IUserManager $userManager,
SettingsMapper $settingsMapper
) {
parent::__construct($time);
$this->connection = $connection;
$this->userManager = $userManager;
$this->settingsMapper = $settingsMapper;
// run every day
$this->setInterval(24 * 60 * 60);
}
protected function run($argument): void {
$query = $this->connection->getQueryBuilder();
$query->select('notification_id')
->from('notifications')
->orderBy('notification_id', 'DESC')
->setMaxResults(1);
$result = $query->executeQuery();
$maxId = (int) $result->fetchOne();
$result->closeCursor();
$this->userManager->callForSeenUsers(function (IUser $user) use ($maxId) {
if ($user->isEnabled()) {
return;
}
try {
$this->settingsMapper->getSettingsByUser($user->getUID());
} catch (DoesNotExistException $e) {
$settings = new Settings();
$settings->setUserId($user->getUID());
$settings->setNextSendTime(1);
$settings->setBatchTime(Settings::EMAIL_SEND_3HOURLY);
$settings->setLastSendId($maxId);
$this->settingsMapper->insert($settings);
}
});
}
}
@@ -0,0 +1,52 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2021, Julien Barnoin <julien@barnoin.com>
*
* @author Julien Barnoin <julien@barnoin.com>
*
* @license AGPL-3.0-or-later
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Notifications\BackgroundJob;
use OCA\Notifications\MailNotifications;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob;
class SendNotificationMails extends TimedJob {
/** @var MailNotifications */
protected $mailNotifications;
/** @var bool */
protected $isCLI;
public function __construct(ITimeFactory $timeFactory,
MailNotifications $mailNotifications,
bool $isCLI) {
parent::__construct($timeFactory);
$this->mailNotifications = $mailNotifications;
$this->isCLI = $isCLI;
}
protected function run($argument): void {
$time = $this->time->getTime();
$batchSize = $this->isCLI ? MailNotifications::BATCH_SIZE_CLI : MailNotifications::BATCH_SIZE_WEB;
$this->mailNotifications->sendEmails($batchSize, $time);
}
}
@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Joas Schilling <coding@schilljs.com>
* @author Kate Döen <kate.doeen@nextcloud.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Notifications;
use OCP\Capabilities\ICapability;
/**
* Class Capabilities
*
* @package OCA\Notifications
*/
class Capabilities implements ICapability {
/**
* Return this classes capabilities
*
* @return array{
* notifications: array{
* ocs-endpoints: string[],
* push: string[],
* admin-notifications: string[],
* },
* }
*/
public function getCapabilities(): array {
return [
'notifications' => [
'ocs-endpoints' => [
'list',
'get',
'delete',
'delete-all',
'icons',
'rich-strings',
'action-web',
'user-status',
'exists',
],
'push' => [
'devices',
'object-data',
'delete',
],
'admin-notifications' => [
'ocs',
'cli',
],
],
];
}
}
@@ -0,0 +1,145 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2017, Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license AGPL-3.0-or-later
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Notifications\Command;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IUser;
use OCP\IUserManager;
use OCP\Notification\IManager;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class Generate extends Command {
/** @var ITimeFactory */
protected $timeFactory;
/** @var IUserManager */
protected $userManager;
/** @var IManager */
protected $notificationManager;
public function __construct(ITimeFactory $timeFactory,
IUserManager $userManager,
IManager $notificationManager) {
parent::__construct();
$this->timeFactory = $timeFactory;
$this->userManager = $userManager;
$this->notificationManager = $notificationManager;
}
protected function configure(): void {
$this
->setName('notification:generate')
->setDescription('Generate a notification for the given user')
->addArgument(
'user-id',
InputArgument::REQUIRED,
'User ID of the user to notify'
)
->addArgument(
'short-message',
InputArgument::REQUIRED,
'Short message to be sent to the user (max. 255 characters)'
)
->addOption(
'long-message',
'l',
InputOption::VALUE_REQUIRED,
'Long mesage to be sent to the user (max. 4000 characters)',
''
)
->addOption(
'dummy',
'd',
InputOption::VALUE_NONE,
'Create a full-flexed dummy notification for client debugging with actions and parameters (short-message will be casted to integer and is the number of actions (max 3))'
)
;
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int {
$userId = $input->getArgument('user-id');
$subject = $input->getArgument('short-message');
$message = $input->getOption('long-message');
$dummy = $input->getOption('dummy');
$user = $this->userManager->get($userId);
if (!$user instanceof IUser) {
$output->writeln('Unknown user');
return 1;
}
if (!$dummy) {
if ($subject === '' || strlen($subject) > 255) {
$output->writeln('Too long or empty short-message');
return 1;
}
if ($message !== '' && strlen($message) > 4000) {
$output->writeln('Too long long-message');
return 1;
}
$subjectTitle = 'cli';
} else {
$subject = (int) $subject;
$subjectTitle = 'dummy';
}
$notification = $this->notificationManager->createNotification();
$datetime = $this->timeFactory->getDateTime();
try {
$notification->setApp('admin_notifications')
->setUser($user->getUID())
->setDateTime($datetime)
->setObject('admin_notifications', dechex($datetime->getTimestamp()))
->setSubject($subjectTitle, [$subject]);
if ($message !== '') {
$notification->setMessage('cli', [$message]);
}
$this->notificationManager->notify($notification);
} catch (\InvalidArgumentException $e) {
$output->writeln('Error while sending the notification');
return 1;
}
return 0;
}
}
@@ -0,0 +1,123 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2017, Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license AGPL-3.0-or-later
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Notifications\Command;
use OCA\Notifications\App;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IUser;
use OCP\IUserManager;
use OCP\Notification\IManager;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class TestPush extends Command {
/** @var ITimeFactory */
protected $timeFactory;
/** @var IUserManager */
protected $userManager;
/** @var IManager */
protected $notificationManager;
/** @var App */
protected $app;
public function __construct(
ITimeFactory $timeFactory,
IUserManager $userManager,
IManager $notificationManager,
App $app) {
parent::__construct();
$this->timeFactory = $timeFactory;
$this->userManager = $userManager;
$this->notificationManager = $notificationManager;
$this->app = $app;
}
protected function configure(): void {
$this
->setName('notification:test-push')
->setDescription('Generate a notification for the given user')
->addArgument(
'user-id',
InputArgument::REQUIRED,
'User ID of the user to notify'
)
->addOption(
'talk',
null,
InputOption::VALUE_NONE,
'Test talk devices'
)
;
}
/**
* @param InputInterface $input
* @param OutputInterface $output
* @return int
*/
protected function execute(InputInterface $input, OutputInterface $output): int {
if (!$this->notificationManager->isFairUseOfFreePushService()) {
$output->writeln('<error>We want to keep offering our push notification service for free, but large</error>');
$output->writeln('<error>users overload our infrastructure. For this reason we have to rate-limit the</error>');
$output->writeln('<error>use of push notifications. If you need this feature, consider using Nextcloud Enterprise.</error>');
return 1;
}
$userId = $input->getArgument('user-id');
$subject = 'Testing push notifications';
$user = $this->userManager->get($userId);
if (!$user instanceof IUser) {
$output->writeln('Unknown user');
return 1;
}
$notification = $this->notificationManager->createNotification();
$datetime = $this->timeFactory->getDateTime();
$app = $input->getOption('talk') ? 'admin_notification_talk' : 'admin_notifications';
try {
$notification->setApp($app)
->setUser($user->getUID())
->setDateTime($datetime)
->setObject('admin_notifications', dechex($datetime->getTimestamp()))
->setSubject('cli', [$subject]);
$this->app->setOutput($output);
$this->notificationManager->notify($notification);
} catch (\InvalidArgumentException $e) {
$output->writeln('Error while sending the notification');
return 1;
}
return 0;
}
}
@@ -0,0 +1,110 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2017, Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license AGPL-3.0-or-later
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Notifications\Controller;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCSController;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IRequest;
use OCP\IUser;
use OCP\IUserManager;
use OCP\Notification\IManager;
class APIController extends OCSController {
/** @var ITimeFactory */
protected $timeFactory;
/** @var IUserManager */
protected $userManager;
/** @var IManager */
protected $notificationManager;
public function __construct(
string $appName,
IRequest $request,
ITimeFactory $timeFactory,
IUserManager $userManager,
IManager $notificationManager,
) {
parent::__construct($appName, $request);
$this->timeFactory = $timeFactory;
$this->userManager = $userManager;
$this->notificationManager = $notificationManager;
}
/**
* Generate a notification for a user
*
* @param string $userId ID of the user
* @param string $shortMessage Subject of the notification
* @param string $longMessage Message of the notification
* @return DataResponse<Http::STATUS_OK, array<empty>, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, null, array{}>
*
* 200: Notification generated successfully
* 400: Generating notification is not possible
* 404: User not found
*/
public function generateNotification(string $userId, string $shortMessage, string $longMessage = ''): DataResponse {
$user = $this->userManager->get($userId);
if (!$user instanceof IUser) {
return new DataResponse(null, Http::STATUS_NOT_FOUND);
}
if ($shortMessage === '' || strlen($shortMessage) > 255) {
return new DataResponse(null, Http::STATUS_BAD_REQUEST);
}
if ($longMessage !== '' && strlen($longMessage) > 4000) {
return new DataResponse(null, Http::STATUS_BAD_REQUEST);
}
$notification = $this->notificationManager->createNotification();
$datetime = $this->timeFactory->getDateTime();
try {
$notification->setApp('admin_notifications')
->setUser($user->getUID())
->setDateTime($datetime)
->setObject('admin_notifications', dechex($datetime->getTimestamp()))
->setSubject('ocs', [$shortMessage]);
if ($longMessage !== '') {
$notification->setMessage('ocs', [$longMessage]);
}
$this->notificationManager->notify($notification);
} catch (\InvalidArgumentException) {
return new DataResponse(null, Http::STATUS_INTERNAL_SERVER_ERROR);
}
return new DataResponse();
}
}
@@ -0,0 +1,365 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023, Joas Schilling <coding@schilljs.com>
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Notifications\Controller;
use OCA\Notifications\Exceptions\NotificationNotFoundException;
use OCA\Notifications\Handler;
use OCA\Notifications\Push;
use OCA\Notifications\ResponseDefinitions;
use OCA\Notifications\Service\ClientService;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCSController;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IRequest;
use OCP\IUser;
use OCP\IUserSession;
use OCP\L10N\IFactory;
use OCP\Notification\IAction;
use OCP\Notification\IManager;
use OCP\Notification\INotification;
use OCP\UserStatus\IManager as IUserStatusManager;
use OCP\UserStatus\IUserStatus;
/**
* @psalm-import-type NotificationsNotification from ResponseDefinitions
* @psalm-import-type NotificationsNotificationAction from ResponseDefinitions
*/
class EndpointController extends OCSController {
public function __construct(
string $appName,
IRequest $request,
protected Handler $handler,
protected IManager $manager,
protected IFactory $l10nFactory,
protected IUserSession $session,
protected ITimeFactory $timeFactory,
protected IUserStatusManager $userStatusManager,
protected ClientService $clientService,
protected Push $push,
) {
parent::__construct($appName, $request);
}
/**
* @NoAdminRequired
* @NoCSRFRequired
*
* Get all notifications
*
* @param string $apiVersion Version of the API to use
* @return DataResponse<Http::STATUS_OK, NotificationsNotification[], array{'X-Nextcloud-User-Status': string}>|DataResponse<Http::STATUS_NO_CONTENT, null, array{X-Nextcloud-User-Status: string}>
*
* 200: Notifications returned
* 204: No app uses notifications
*/
public function listNotifications(string $apiVersion): DataResponse {
$userStatus = $this->userStatusManager->getUserStatuses([
$this->getCurrentUser(),
]);
$headers = ['X-Nextcloud-User-Status' => IUserStatus::ONLINE];
if (isset($userStatus[$this->getCurrentUser()])) {
$userStatus = $userStatus[$this->getCurrentUser()];
$headers['X-Nextcloud-User-Status'] = $userStatus->getStatus();
}
// When there are no apps registered that use the notifications
// We stop polling for them.
if (!$this->manager->hasNotifiers()) {
return new DataResponse(null, Http::STATUS_NO_CONTENT, $headers);
}
$user = $this->session->getUser();
$filter = $this->manager->createNotification();
$filter->setUser($this->getCurrentUser());
$language = $this->l10nFactory->getUserLanguage($user);
$notifications = $this->handler->get($filter);
$shouldFlush = $this->manager->defer();
$hasActiveTalkDesktop = false;
if ($user instanceof IUser) {
$hasActiveTalkDesktop = $this->clientService->hasTalkDesktop(
$user->getUID(),
$this->timeFactory->getTime() - ClientService::DESKTOP_CLIENT_TIMEOUT
);
}
$data = [];
$notificationIds = [];
foreach ($notifications as $notificationId => $notification) {
/** @var INotification $notification */
try {
$notification = $this->manager->prepare($notification, $language);
} catch (\InvalidArgumentException) {
// The app was disabled, skip the notification
continue;
}
$notificationIds[] = $notificationId;
$data[] = $this->notificationToArray($notificationId, $notification, $apiVersion, $hasActiveTalkDesktop);
}
if ($shouldFlush) {
$this->manager->flush();
}
$eTag = $this->generateETag($notificationIds);
$response = new DataResponse($data, Http::STATUS_OK, $headers);
if ($apiVersion !== 'v1') {
$response->setETag($eTag);
}
return $response;
}
/**
* @NoAdminRequired
* @NoCSRFRequired
*
* Get a notification
*
* @param string $apiVersion Version of the API to use
* @param int $id ID of the notification
* @return DataResponse<Http::STATUS_OK, NotificationsNotification, array{}>|DataResponse<Http::STATUS_NOT_FOUND, null, array{}>
*
* 200: Notification returned
* 404: Notification not found
*/
public function getNotification(string $apiVersion, int $id): DataResponse {
if (!$this->manager->hasNotifiers()) {
return new DataResponse(null, Http::STATUS_NOT_FOUND);
}
if ($id === 0) {
return new DataResponse(null, Http::STATUS_NOT_FOUND);
}
try {
$notification = $this->handler->getById($id, $this->getCurrentUser());
} catch (NotificationNotFoundException $e) {
return new DataResponse(null, Http::STATUS_NOT_FOUND);
}
$user = $this->session->getUser();
$language = $this->l10nFactory->getUserLanguage($user);
try {
$notification = $this->manager->prepare($notification, $language);
} catch (\InvalidArgumentException $e) {
// The app was disabled
return new DataResponse(null, Http::STATUS_NOT_FOUND);
}
$hasActiveTalkDesktop = false;
if ($user instanceof IUser) {
$hasActiveTalkDesktop = $this->clientService->hasTalkDesktop(
$user->getUID(),
$this->timeFactory->getTime() - ClientService::DESKTOP_CLIENT_TIMEOUT
);
}
return new DataResponse($this->notificationToArray($id, $notification, $apiVersion, $hasActiveTalkDesktop));
}
/**
* @NoAdminRequired
*
* Check if notification IDs exist
*
* @param string $apiVersion Version of the API to use
* @param int[] $ids IDs of the notifications to check
* @return DataResponse<Http::STATUS_OK|Http::STATUS_BAD_REQUEST, int[], array{}>
*
* 200: Existing notification IDs returned
* 400: Too many notification IDs requested
*/
public function confirmIdsForUser(string $apiVersion, array $ids): DataResponse {
if (!$this->manager->hasNotifiers()) {
return new DataResponse([], Http::STATUS_OK);
}
if (empty($ids)) {
return new DataResponse([], Http::STATUS_OK);
}
if (count($ids) > 200) {
return new DataResponse([], Http::STATUS_BAD_REQUEST);
}
$ids = array_unique(array_filter(array_map(
static fn ($id) => is_numeric($id) ? (int) $id : 0,
$ids
)));
$existingIds = $this->handler->confirmIdsForUser($this->getCurrentUser(), $ids);
return new DataResponse($existingIds, Http::STATUS_OK);
}
/**
* @NoAdminRequired
*
* Delete a notification
*
* @param int $id ID of the notification
* @return DataResponse<Http::STATUS_OK, array<empty>, array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_NOT_FOUND, null, array{}>
*
* 200: Notification deleted successfully
* 403: Deleting notification for impersonated user is not allowed
* 404: Notification not found
*/
public function deleteNotification(int $id): DataResponse {
if ($id === 0) {
return new DataResponse(null, Http::STATUS_NOT_FOUND);
}
if ($this->session->getImpersonatingUserID() !== null) {
return new DataResponse(null, Http::STATUS_FORBIDDEN);
}
try {
$notification = $this->handler->getById($id, $this->getCurrentUser());
$deleted = $this->handler->deleteById($id, $this->getCurrentUser(), $notification);
if ($deleted) {
$this->push->pushDeleteToDevice($this->getCurrentUser(), [$id], $notification->getApp());
}
} catch (NotificationNotFoundException $e) {
}
return new DataResponse();
}
/**
* @NoAdminRequired
*
* Delete all notifications
*
* @return DataResponse<Http::STATUS_OK, array<empty>, array{}>|DataResponse<Http::STATUS_FORBIDDEN, null, array{}>
*
* 200: All notifications deleted successfully
* 403: Deleting notification for impersonated user is not allowed
*/
public function deleteAllNotifications(): DataResponse {
if ($this->session->getImpersonatingUserID() !== null) {
return new DataResponse(null, Http::STATUS_FORBIDDEN);
}
$shouldFlush = $this->manager->defer();
$deletedSomething = $this->handler->deleteByUser($this->getCurrentUser());
if ($deletedSomething) {
$this->push->pushDeleteToDevice($this->getCurrentUser(), null);
}
if ($shouldFlush) {
$this->manager->flush();
}
return new DataResponse();
}
/**
* Get an ETag for the notification ids
*
* @param array $notifications
* @return string
*/
protected function generateETag(array $notifications): string {
return md5(json_encode($notifications));
}
/**
* @param int $notificationId
* @param INotification $notification
* @param string $apiVersion
* @param bool $hasActiveTalkDesktop
* @return NotificationsNotification
*/
protected function notificationToArray(int $notificationId, INotification $notification, string $apiVersion, bool $hasActiveTalkDesktop = false): array {
$data = [
'notification_id' => $notificationId,
'app' => $notification->getApp(),
'user' => $notification->getUser(),
'datetime' => $notification->getDateTime()->format('c'),
'object_type' => $notification->getObjectType(),
'object_id' => $notification->getObjectId(),
'subject' => $notification->getParsedSubject(),
'message' => $notification->getParsedMessage(),
'link' => $notification->getLink(),
];
if ($apiVersion !== 'v1') {
if ($this->request->isUserAgent([IRequest::USER_AGENT_TALK_DESKTOP])) {
$shouldNotify = $notification->getApp() === 'spreed';
} else {
$shouldNotify = !$hasActiveTalkDesktop || $notification->getApp() !== 'spreed';
}
$data = array_merge($data, [
'subjectRich' => $notification->getRichSubject(),
'subjectRichParameters' => $notification->getRichSubjectParameters(),
'messageRich' => $notification->getRichMessage(),
'messageRichParameters' => $notification->getRichMessageParameters(),
'icon' => $notification->getIcon(),
'shouldNotify' => $shouldNotify,
]);
}
$data['actions'] = [];
foreach ($notification->getParsedActions() as $action) {
$data['actions'][] = $this->actionToArray($action);
}
return $data;
}
/**
* @param IAction $action
* @return NotificationsNotificationAction
*/
protected function actionToArray(IAction $action): array {
return [
'label' => $action->getParsedLabel(),
'link' => $action->getLink(),
'type' => $action->getRequestType(),
'primary' => $action->isPrimary(),
];
}
/**
* @return string
*/
protected function getCurrentUser(): string {
$user = $this->session->getUser();
if ($user instanceof IUser) {
$user = $user->getUID();
}
return (string) $user;
}
}
@@ -0,0 +1,305 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2017, Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license AGPL-3.0-or-later
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Notifications\Controller;
use OC\Authentication\Exceptions\InvalidTokenException;
use OC\Authentication\Token\IProvider;
use OC\Authentication\Token\IToken;
use OC\Security\IdentityProof\Manager;
use OCA\Notifications\ResponseDefinitions;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCSController;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\IRequest;
use OCP\ISession;
use OCP\IUser;
use OCP\IUserSession;
/**
* @psalm-import-type NotificationsPushDevice from ResponseDefinitions
*/
class PushController extends OCSController {
/** @var IDBConnection */
private $db;
/** @var ISession */
private $session;
/** @var IUserSession */
private $userSession;
/** @var IProvider */
private $tokenProvider;
/** @var Manager */
private $identityProof;
public function __construct(string $appName,
IRequest $request,
IDBConnection $db,
ISession $session,
IUserSession $userSession,
IProvider $tokenProvider,
Manager $identityProof) {
parent::__construct($appName, $request);
$this->db = $db;
$this->session = $session;
$this->userSession = $userSession;
$this->tokenProvider = $tokenProvider;
$this->identityProof = $identityProof;
}
/**
* @NoAdminRequired
*
* Register device for push notifications
*
* @param string $pushTokenHash Hash of the push token
* @param string $devicePublicKey Public key of the device
* @param string $proxyServer Proxy server to be used
* @return DataResponse<Http::STATUS_OK|Http::STATUS_CREATED, NotificationsPushDevice, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{message: string}, array{}>|DataResponse<Http::STATUS_UNAUTHORIZED, array<empty>, array{}>
*
* 200: Device was already registered
* 201: Device registered successfully
* 400: Registering device is not possible
* 401: Missing permissions to register device
*/
public function registerDevice(string $pushTokenHash, string $devicePublicKey, string $proxyServer): DataResponse {
$user = $this->userSession->getUser();
if (!$user instanceof IUser) {
return new DataResponse([], Http::STATUS_UNAUTHORIZED);
}
if (!preg_match('/^([a-f0-9]{128})$/', $pushTokenHash)) {
return new DataResponse(['message' => 'INVALID_PUSHTOKEN_HASH'], Http::STATUS_BAD_REQUEST);
}
if (
strpos($devicePublicKey, '-----BEGIN PUBLIC KEY-----' . "\n") !== 0 ||
((\strlen($devicePublicKey) !== 450 || strpos($devicePublicKey, "\n" . '-----END PUBLIC KEY-----') !== 425) &&
(\strlen($devicePublicKey) !== 451 || strpos($devicePublicKey, "\n" . '-----END PUBLIC KEY-----' . "\n") !== 425))
) {
return new DataResponse(['message' => 'INVALID_DEVICE_KEY'], Http::STATUS_BAD_REQUEST);
}
if (
!filter_var($proxyServer, FILTER_VALIDATE_URL) ||
\strlen($proxyServer) > 256 ||
!preg_match('/^(https\:\/\/|http\:\/\/(localhost|[a-z0-9\.-]*\.(internal|local))(\:\d{0,5})?\/)/', $proxyServer)
) {
return new DataResponse(['message' => 'INVALID_PROXY_SERVER'], Http::STATUS_BAD_REQUEST);
}
$tokenId = $this->session->get('token-id');
if (!\is_int($tokenId)) {
return new DataResponse(['message' => 'INVALID_SESSION_TOKEN'], Http::STATUS_BAD_REQUEST);
}
try {
$token = $this->tokenProvider->getTokenById($tokenId);
} catch (InvalidTokenException $e) {
return new DataResponse(['message' => 'INVALID_SESSION_TOKEN'], Http::STATUS_BAD_REQUEST);
}
$key = $this->identityProof->getKey($user);
$deviceIdentifier = json_encode([$user->getCloudId(), $token->getId()]);
openssl_sign($deviceIdentifier, $signature, $key->getPrivate(), OPENSSL_ALGO_SHA512);
/**
* For some reason the push proxy's golang code needs the signature
* of the deviceIdentifier before the sha512 hashing. Assumption is that
* openssl_sign already does the sha512 internally.
*/
$deviceIdentifier = base64_encode(hash('sha512', $deviceIdentifier, true));
$appType = 'unknown';
if ($this->request->isUserAgent([
IRequest::USER_AGENT_TALK_ANDROID,
IRequest::USER_AGENT_TALK_IOS,
])) {
$appType = 'talk';
} elseif ($this->request->isUserAgent([
IRequest::USER_AGENT_CLIENT_ANDROID,
IRequest::USER_AGENT_CLIENT_IOS,
])) {
$appType = 'nextcloud';
}
$created = $this->savePushToken($user, $token, $deviceIdentifier, $devicePublicKey, $pushTokenHash, $proxyServer, $appType);
return new DataResponse([
'publicKey' => $key->getPublic(),
'deviceIdentifier' => $deviceIdentifier,
'signature' => base64_encode($signature),
], $created ? Http::STATUS_CREATED : Http::STATUS_OK);
}
/**
* @NoAdminRequired
*
* Remove a device from push notifications
*
* @return DataResponse<Http::STATUS_OK|Http::STATUS_ACCEPTED|Http::STATUS_UNAUTHORIZED, array<empty>, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{message: string}, array{}>
*
* 200: No device registered
* 202: Device removed successfully
* 400: Removing device is not possible
* 401: Missing permissions to remove device
*/
public function removeDevice(): DataResponse {
$user = $this->userSession->getUser();
if (!$user instanceof IUser) {
return new DataResponse([], Http::STATUS_UNAUTHORIZED);
}
$tokenId = (int)$this->session->get('token-id');
try {
$token = $this->tokenProvider->getTokenById($tokenId);
} catch (InvalidTokenException $e) {
return new DataResponse(['message' => 'INVALID_SESSION_TOKEN'], Http::STATUS_BAD_REQUEST);
}
if ($this->deletePushToken($user, $token)) {
return new DataResponse([], Http::STATUS_ACCEPTED);
}
return new DataResponse([], Http::STATUS_OK);
}
/**
* @param IUser $user
* @param IToken $token
* @param string $deviceIdentifier
* @param string $devicePublicKey
* @param string $pushTokenHash
* @param string $proxyServer
* @param string $appType
* @return bool If the hash was new to the database
*/
protected function savePushToken(IUser $user, IToken $token, string $deviceIdentifier, string $devicePublicKey, string $pushTokenHash, string $proxyServer, string $appType): bool {
$query = $this->db->getQueryBuilder();
$query->select('*')
->from('notifications_pushhash')
->where($query->expr()->eq('uid', $query->createNamedParameter($user->getUID())))
->andWhere($query->expr()->eq('token', $query->createNamedParameter($token->getId())));
$result = $query->execute();
$row = $result->fetch();
$result->closeCursor();
if (!$row) {
// In case the auth token is new, delete potentially old entries for the same device (push token) by this user
$this->deletePushTokenByHash($user, $pushTokenHash);
return $this->insertPushToken($user, $token, $deviceIdentifier, $devicePublicKey, $pushTokenHash, $proxyServer, $appType);
}
return $this->updatePushToken($user, $token, $devicePublicKey, $pushTokenHash, $proxyServer, $appType);
}
/**
* @param IUser $user
* @param IToken $token
* @param string $deviceIdentifier
* @param string $devicePublicKey
* @param string $pushTokenHash
* @param string $proxyServer
* @param string $appType
* @return bool If the entry was created
*/
protected function insertPushToken(IUser $user, IToken $token, string $deviceIdentifier, string $devicePublicKey, string $pushTokenHash, string $proxyServer, string $appType): bool {
$devicePublicKeyHash = hash('sha512', $devicePublicKey);
$query = $this->db->getQueryBuilder();
$query->insert('notifications_pushhash')
->values([
'uid' => $query->createNamedParameter($user->getUID()),
'token' => $query->createNamedParameter($token->getId(), IQueryBuilder::PARAM_INT),
'deviceidentifier' => $query->createNamedParameter($deviceIdentifier),
'devicepublickey' => $query->createNamedParameter($devicePublicKey),
'devicepublickeyhash' => $query->createNamedParameter($devicePublicKeyHash),
'pushtokenhash' => $query->createNamedParameter($pushTokenHash),
'proxyserver' => $query->createNamedParameter($proxyServer),
'apptype' => $query->createNamedParameter($appType),
]);
return $query->executeStatement() > 0;
}
/**
* @param IUser $user
* @param IToken $token
* @param string $devicePublicKey
* @param string $pushTokenHash
* @param string $proxyServer
* @param string $appType
* @return bool If the entry was updated
*/
protected function updatePushToken(IUser $user, IToken $token, string $devicePublicKey, string $pushTokenHash, string $proxyServer, string $appType): bool {
$devicePublicKeyHash = hash('sha512', $devicePublicKey);
$query = $this->db->getQueryBuilder();
$query->update('notifications_pushhash')
->set('devicepublickey', $query->createNamedParameter($devicePublicKey))
->set('devicepublickeyhash', $query->createNamedParameter($devicePublicKeyHash))
->set('pushtokenhash', $query->createNamedParameter($pushTokenHash))
->set('proxyserver', $query->createNamedParameter($proxyServer))
->set('apptype', $query->createNamedParameter($appType))
->where($query->expr()->eq('uid', $query->createNamedParameter($user->getUID())))
->andWhere($query->expr()->eq('token', $query->createNamedParameter($token->getId(), IQueryBuilder::PARAM_INT)));
return $query->executeStatement() !== 0;
}
/**
* @param IUser $user
* @param IToken $token
* @return bool If the entry was deleted
*/
protected function deletePushToken(IUser $user, IToken $token): bool {
$query = $this->db->getQueryBuilder();
$query->delete('notifications_pushhash')
->where($query->expr()->eq('uid', $query->createNamedParameter($user->getUID())))
->andWhere($query->expr()->eq('token', $query->createNamedParameter($token->getId(), IQueryBuilder::PARAM_INT)));
return $query->executeStatement() !== 0;
}
/**
* @param IUser $user
* @param string $pushTokenHash
* @return bool If the entry was deleted
*/
protected function deletePushTokenByHash(IUser $user, string $pushTokenHash): bool {
$query = $this->db->getQueryBuilder();
$query->delete('notifications_pushhash')
->where($query->expr()->eq('uid', $query->createNamedParameter($user->getUID())))
->andWhere($query->expr()->eq('pushtokenhash', $query->createNamedParameter($pushTokenHash)));
return $query->executeStatement() !== 0;
}
}
@@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2021, Julien Barnoin <julien@barnoin.com>
*
* @author Julien Barnoin <julien@barnoin.com>
*
* @license AGPL-3.0-or-later
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Notifications\Controller;
use OCA\Notifications\AppInfo\Application;
use OCA\Notifications\Model\SettingsMapper;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCSController;
use OCP\IConfig;
use OCP\IRequest;
class SettingsController extends OCSController {
protected IConfig $config;
protected SettingsMapper $settingsMapper;
protected string $userId;
public function __construct(string $appName,
IRequest $request,
IConfig $config,
SettingsMapper $settingsMapper,
string $userId) {
parent::__construct($appName, $request);
$this->config = $config;
$this->settingsMapper = $settingsMapper;
$this->userId = $userId;
}
/**
* @NoAdminRequired
*
* Update personal notification settings
*
* @param int $batchSetting How often E-mails about missed notifications should be sent (hourly: 1; every three hours: 2; daily: 3; weekly: 4)
* @param string $soundNotification Enable sound for notifications ('yes' or 'no')
* @param string $soundTalk Enable sound for Talk notifications ('yes' or 'no')
* @return DataResponse<Http::STATUS_OK, array<empty>, array{}>
*
* 200: Personal settings updated
*/
public function personal(int $batchSetting, string $soundNotification, string $soundTalk): DataResponse {
$this->settingsMapper->setBatchSettingForUser($this->userId, $batchSetting);
$this->config->setUserValue($this->userId, Application::APP_ID, 'sound_notification', $soundNotification !== 'no' ? 'yes' : 'no');
$this->config->setUserValue($this->userId, Application::APP_ID, 'sound_talk', $soundTalk !== 'no' ? 'yes' : 'no');
return new DataResponse();
}
/**
* @AuthorizedAdminSetting(settings=OCA\Notifications\Settings\Admin)
*
* Update default notification settings for new users
*
* @param int $batchSetting How often E-mails about missed notifications should be sent (hourly: 1; every three hours: 2; daily: 3; weekly: 4)
* @param string $soundNotification Enable sound for notifications ('yes' or 'no')
* @param string $soundTalk Enable sound for Talk notifications ('yes' or 'no')
* @return DataResponse<Http::STATUS_OK, array<empty>, array{}>
*
* 200: Admin settings updated
*/
public function admin(int $batchSetting, string $soundNotification, string $soundTalk): DataResponse {
$this->config->setAppValue(Application::APP_ID, 'setting_batchtime', (string) $batchSetting);
$this->config->setAppValue(Application::APP_ID, 'sound_notification', $soundNotification !== 'no' ? 'yes' : 'no');
$this->config->setAppValue(Application::APP_ID, 'sound_talk', $soundTalk !== 'no' ? 'yes' : 'no');
return new DataResponse();
}
}
@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2018, Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license AGPL-3.0-or-later
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Notifications\Exceptions;
class NotificationNotFoundException extends \OutOfBoundsException {
}
@@ -0,0 +1,144 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2022, Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license AGPL-3.0-or-later
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Notifications;
use OCP\IUser;
class FakeUser implements IUser {
protected string $userId;
public function __construct(string $userId) {
$this->userId = $userId;
}
public function getUID(): string {
return $this->userId;
}
public function getCloudId() {
throw new \RuntimeException('Not implemented');
}
public function getSystemEMailAddress(): ?string {
throw new \RuntimeException('Not implemented');
}
public function getPrimaryEMailAddress(): ?string {
throw new \RuntimeException('Not implemented');
}
public function getDisplayName() {
throw new \RuntimeException('Not implemented');
}
public function setDisplayName($displayName) {
throw new \RuntimeException('Not implemented');
}
public function getLastLogin() {
throw new \RuntimeException('Not implemented');
}
public function updateLastLoginTimestamp() {
throw new \RuntimeException('Not implemented');
}
public function delete() {
throw new \RuntimeException('Not implemented');
}
public function setPassword($password, $recoveryPassword = null) {
throw new \RuntimeException('Not implemented');
}
public function getHome() {
throw new \RuntimeException('Not implemented');
}
public function getBackendClassName() {
throw new \RuntimeException('Not implemented');
}
public function getBackend(): ?\OCP\UserInterface {
throw new \RuntimeException('Not implemented');
}
public function canChangeAvatar() {
throw new \RuntimeException('Not implemented');
}
public function canChangePassword() {
throw new \RuntimeException('Not implemented');
}
public function canChangeDisplayName() {
throw new \RuntimeException('Not implemented');
}
public function isEnabled() {
throw new \RuntimeException('Not implemented');
}
public function setEnabled(bool $enabled = true) {
throw new \RuntimeException('Not implemented');
}
public function getEMailAddress() {
throw new \RuntimeException('Not implemented');
}
public function getAvatarImage($size) {
throw new \RuntimeException('Not implemented');
}
public function setEMailAddress($mailAddress) {
throw new \RuntimeException('Not implemented');
}
public function getQuota() {
throw new \RuntimeException('Not implemented');
}
public function setQuota($quota) {
throw new \RuntimeException('Not implemented');
}
public function setSystemEMailAddress(string $mailAddress): void {
throw new \RuntimeException('Not implemented');
}
public function setPrimaryEMailAddress(string $mailAddress): void {
throw new \RuntimeException('Not implemented');
}
public function getManagerUids(): array {
throw new \RuntimeException('Not implemented');
}
public function setManagerUids(array $uids): void {
throw new \RuntimeException('Not implemented');
}
}
@@ -0,0 +1,425 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023, Joas Schilling <coding@schilljs.com>
* @copyright Copyright (c) 2016, ownCloud, Inc.
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, version 3,
* as published by the Free Software Foundation.
*
* 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, version 3,
* along with this program. If not, see <http://www.gnu.org/licenses/>
*
*/
namespace OCA\Notifications;
use OCA\Notifications\Exceptions\NotificationNotFoundException;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\Notification\IAction;
use OCP\Notification\IManager;
use OCP\Notification\INotification;
class Handler {
/** @var IDBConnection */
protected $connection;
/** @var IManager */
protected $manager;
public function __construct(IDBConnection $connection,
IManager $manager) {
$this->connection = $connection;
$this->manager = $manager;
}
/**
* Add a new notification to the database
*
* @param INotification $notification
* @return int
*/
public function add(INotification $notification): int {
$sql = $this->connection->getQueryBuilder();
$sql->insert('notifications');
$this->sqlInsert($sql, $notification);
$sql->executeStatement();
return $sql->getLastInsertId();
}
/**
* Count the notifications matching the given Notification
*
* @param INotification $notification
* @return int
*/
public function count(INotification $notification): int {
$sql = $this->connection->getQueryBuilder();
$sql->select($sql->createFunction('COUNT(*)'))
->from('notifications');
$this->sqlWhere($sql, $notification);
$statement = $sql->executeQuery();
$count = (int) $statement->fetchOne();
$statement->closeCursor();
return $count;
}
/**
* Delete the notifications matching the given Notification
*
* @param INotification $notification
* @return array A Map with all deleted notifications [user => [notifications]]
*/
public function delete(INotification $notification): array {
$sql = $this->connection->getQueryBuilder();
$sql->select('*')
->from('notifications');
$this->sqlWhere($sql, $notification);
$statement = $sql->executeQuery();
$deleted = [];
$notifications = [];
while ($row = $statement->fetch()) {
if (!isset($deleted[$row['user']])) {
$deleted[$row['user']] = [];
}
$deleted[$row['user']][] = [
'id' => (int) $row['notification_id'],
'app' => $row['app'],
];
$notifications[(int) $row['notification_id']] = $this->notificationFromRow($row);
}
$statement->closeCursor();
if (count($notifications) === 0) {
return [];
}
$this->connection->beginTransaction();
try {
$shouldFlush = $this->manager->defer();
foreach ($notifications as $n) {
$this->manager->dismissNotification($n);
}
$notificationIds = array_keys($notifications);
foreach (array_chunk($notificationIds, 1000) as $chunk) {
$this->deleteIds($chunk);
}
if ($shouldFlush) {
$this->manager->flush();
}
} catch (\Throwable $e) {
$this->connection->rollBack();
throw $e;
}
$this->connection->commit();
return $deleted;
}
/**
* Delete the notification of a given user
*
* @param string $user
* @return bool
*/
public function deleteByUser(string $user): bool {
$notification = $this->manager->createNotification();
try {
$notification->setUser($user);
} catch (\InvalidArgumentException $e) {
return false;
}
return !empty($this->delete($notification));
}
/**
* Delete the notification matching the given id
*
* @param int $id
* @param string $user
* @param INotification|null $notification
* @return bool
* @throws NotificationNotFoundException
*/
public function deleteById(int $id, string $user, ?INotification $notification = null): bool {
if (!$notification instanceof INotification) {
$notification = $this->getById($id, $user);
}
$this->manager->dismissNotification($notification);
$sql = $this->connection->getQueryBuilder();
$sql->delete('notifications')
->where($sql->expr()->eq('notification_id', $sql->createNamedParameter($id)))
->andWhere($sql->expr()->eq('user', $sql->createNamedParameter($user)));
return (bool) $sql->executeStatement();
}
/**
* Delete the notification matching the given ids
*
* @param int[] $ids
*/
public function deleteIds(array $ids): void {
$sql = $this->connection->getQueryBuilder();
$sql->delete('notifications')
->where($sql->expr()->in('notification_id', $sql->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY)));
$sql->executeStatement();
}
/**
* Get the notification matching the given id
*
* @param int $id
* @param string $user
* @return INotification
* @throws NotificationNotFoundException
*/
public function getById(int $id, string $user): INotification {
$sql = $this->connection->getQueryBuilder();
$sql->select('*')
->from('notifications')
->where($sql->expr()->eq('notification_id', $sql->createNamedParameter($id)))
->andWhere($sql->expr()->eq('user', $sql->createNamedParameter($user)));
$statement = $sql->executeQuery();
$row = $statement->fetch();
$statement->closeCursor();
if ($row === false) {
throw new NotificationNotFoundException('No entry returned from database');
}
try {
return $this->notificationFromRow($row);
} catch (\InvalidArgumentException $e) {
throw new NotificationNotFoundException('Could not create notification from database row');
}
}
/**
* Confirm that the notification ids still exist for the user
*
* @param string $user
* @param int[] $ids
* @return int[]
*/
public function confirmIdsForUser(string $user, array $ids): array {
$query = $this->connection->getQueryBuilder();
$query->select('notification_id')
->from('notifications')
->where($query->expr()->in('notification_id', $query->createNamedParameter($ids, IQueryBuilder::PARAM_INT_ARRAY)))
->andWhere($query->expr()->eq('user', $query->createNamedParameter($user)));
$result = $query->executeQuery();
$existing = [];
while ($row = $result->fetch()) {
$existing[] = (int) $row['notification_id'];
}
$result->closeCursor();
return $existing;
}
/**
* Get the notifications after (and excluding) the given id
*
* @param int $startAfterId
* @param string $userId
* @param int $limit
* @return array [notification_id => INotification]
*/
public function getAfterId(int $startAfterId, string $userId, int $limit = 25): array {
$sql = $this->connection->getQueryBuilder();
$sql->select('*')
->from('notifications')
->where($sql->expr()->gt('notification_id', $sql->createNamedParameter($startAfterId)))
->andWhere($sql->expr()->eq('user', $sql->createNamedParameter($userId)))
->orderBy('notification_id', 'DESC')
->setMaxResults($limit);
$statement = $sql->executeQuery();
$notifications = [];
while ($row = $statement->fetch()) {
try {
$notifications[(int)$row['notification_id']] = $this->notificationFromRow($row);
} catch (\InvalidArgumentException $e) {
continue;
}
}
$statement->closeCursor();
return $notifications;
}
/**
* Return the notifications matching the given Notification
*
* @param INotification $notification
* @param int $limit
* @return array [notification_id => INotification]
*/
public function get(INotification $notification, $limit = 25): array {
$sql = $this->connection->getQueryBuilder();
$sql->select('*')
->from('notifications')
->orderBy('notification_id', 'DESC')
->setMaxResults($limit);
$this->sqlWhere($sql, $notification);
$statement = $sql->executeQuery();
$notifications = [];
while ($row = $statement->fetch()) {
try {
$notifications[(int)$row['notification_id']] = $this->notificationFromRow($row);
} catch (\InvalidArgumentException $e) {
continue;
}
}
$statement->closeCursor();
return $notifications;
}
/**
* Add where statements to a query builder matching the given notification
*
* @param IQueryBuilder $sql
* @param INotification $notification
*/
protected function sqlWhere(IQueryBuilder $sql, INotification $notification) {
if ($notification->getApp() !== '') {
$sql->andWhere($sql->expr()->eq('app', $sql->createNamedParameter($notification->getApp())));
}
if ($notification->getUser() !== '') {
$sql->andWhere($sql->expr()->eq('user', $sql->createNamedParameter($notification->getUser())));
}
$timestamp = $notification->getDateTime()->getTimestamp();
if ($timestamp !== 0) {
$sql->andWhere($sql->expr()->eq('timestamp', $sql->createNamedParameter($timestamp)));
}
if ($notification->getObjectType() !== '') {
$sql->andWhere($sql->expr()->eq('object_type', $sql->createNamedParameter($notification->getObjectType())));
}
if ($notification->getObjectId() !== '') {
$sql->andWhere($sql->expr()->eq('object_id', $sql->createNamedParameter($notification->getObjectId())));
}
if ($notification->getSubject() !== '') {
$sql->andWhere($sql->expr()->eq('subject', $sql->createNamedParameter($notification->getSubject())));
}
if ($notification->getMessage() !== '') {
$sql->andWhere($sql->expr()->eq('message', $sql->createNamedParameter($notification->getMessage())));
}
if ($notification->getLink() !== '') {
$sql->andWhere($sql->expr()->eq('link', $sql->createNamedParameter($notification->getLink())));
}
if ($notification->getIcon() !== '') {
$sql->andWhere($sql->expr()->eq('icon', $sql->createNamedParameter($notification->getIcon())));
}
}
/**
* Turn a notification into an input statement
*
* @param IQueryBuilder $sql
* @param INotification $notification
*/
protected function sqlInsert(IQueryBuilder $sql, INotification $notification) {
$actions = [];
foreach ($notification->getActions() as $action) {
/** @var IAction $action */
$actions[] = [
'label' => $action->getLabel(),
'link' => $action->getLink(),
'type' => $action->getRequestType(),
'primary' => $action->isPrimary(),
];
}
$sql->setValue('app', $sql->createNamedParameter($notification->getApp()))
->setValue('user', $sql->createNamedParameter($notification->getUser()))
->setValue('timestamp', $sql->createNamedParameter($notification->getDateTime()->getTimestamp()))
->setValue('object_type', $sql->createNamedParameter($notification->getObjectType()))
->setValue('object_id', $sql->createNamedParameter($notification->getObjectId()))
->setValue('subject', $sql->createNamedParameter($notification->getSubject()))
->setValue('subject_parameters', $sql->createNamedParameter(json_encode($notification->getSubjectParameters())))
->setValue('message', $sql->createNamedParameter($notification->getMessage()))
->setValue('message_parameters', $sql->createNamedParameter(json_encode($notification->getMessageParameters())))
->setValue('link', $sql->createNamedParameter($notification->getLink()))
->setValue('icon', $sql->createNamedParameter($notification->getIcon()))
->setValue('actions', $sql->createNamedParameter(json_encode($actions)));
}
/**
* Turn a database row into a INotification
*
* @param array $row
* @return INotification
* @throws \InvalidArgumentException
*/
protected function notificationFromRow(array $row): INotification {
$dateTime = new \DateTime();
$dateTime->setTimestamp((int) $row['timestamp']);
$notification = $this->manager->createNotification();
$notification->setApp($row['app'])
->setUser($row['user'])
->setDateTime($dateTime)
->setObject($row['object_type'], $row['object_id'])
->setSubject($row['subject'], (array) json_decode($row['subject_parameters'], true));
if ($row['message'] !== '' && $row['message'] !== null) {
$notification->setMessage($row['message'], (array) json_decode($row['message_parameters'], true));
}
if ($row['link'] !== '' && $row['link'] !== null) {
$notification->setLink($row['link']);
}
if ($row['icon'] !== '' && $row['icon'] !== null) {
$notification->setIcon($row['icon']);
}
$actions = (array) json_decode($row['actions'], true);
foreach ($actions as $actionData) {
$action = $notification->createAction();
$action->setLabel($actionData['label'])
->setLink($actionData['link'], $actionData['type']);
if (isset($actionData['primary'])) {
$action->setPrimary($actionData['primary']);
}
$notification->addAction($action);
}
return $notification;
}
}
@@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2021, Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license AGPL-3.0-or-later
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Notifications\Listener;
use OCA\Notifications\AppInfo\Application;
use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\IConfig;
use OCP\IUser;
use OCP\IUserSession;
use OCP\Notification\IManager;
use OCP\Util;
/**
* @template-implements IEventListener<Event|BeforeTemplateRenderedEvent>
*/
class BeforeTemplateRenderedListener implements IEventListener {
protected IConfig $config;
protected IUserSession $userSession;
protected IInitialState $initialState;
protected IManager $notificationManager;
public function __construct(IConfig $config,
IUserSession $userSession,
IInitialState $initialState,
IManager $notificationManager) {
$this->config = $config;
$this->userSession = $userSession;
$this->initialState = $initialState;
$this->notificationManager = $notificationManager;
}
public function handle(Event $event): void {
if (!($event instanceof BeforeTemplateRenderedEvent)) {
// Unrelated
return;
}
if ($event->getResponse()->getRenderAs() !== TemplateResponse::RENDER_AS_USER) {
return;
}
if (!$this->userSession->getUser() instanceof IUser) {
return;
}
$this->initialState->provideInitialState(
'sound_notification',
$this->config->getUserValue(
$this->userSession->getUser()->getUID(),
Application::APP_ID,
'sound_notification',
'yes'
) === 'yes'
);
$this->initialState->provideInitialState(
'sound_talk',
$this->config->getUserValue(
$this->userSession->getUser()->getUID(),
Application::APP_ID,
'sound_talk',
'yes'
) === 'yes'
);
/**
* We want to keep offering our push notification service for free, but large
* users overload our infrastructure. For this reason we have to rate-limit the
* use of push notifications. If you need this feature, consider using Nextcloud Enterprise.
*/
$this->initialState->provideInitialState(
'throttled_push_notifications',
!$this->notificationManager->isFairUseOfFreePushService()
);
Util::addScript('notifications', 'notifications-main');
}
}
@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2022, Nikita Toponen <natoponen@gmail.com>
*
* @author Nikita Toponen <natoponen@gmail.com>
*
* @license AGPL-3.0-or-later
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Notifications\Listener;
use OCA\Notifications\AppInfo\Application;
use OCA\Notifications\Model\Settings;
use OCA\Notifications\Model\SettingsMapper;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\IConfig;
use OCP\User\Events\PostLoginEvent;
/**
* @template-implements IEventListener<Event|PostLoginEvent>
*/
class PostLoginListener implements IEventListener {
private SettingsMapper $settingsMapper;
private IConfig $config;
public function __construct(SettingsMapper $settingsMapper, IConfig $config) {
$this->settingsMapper = $settingsMapper;
$this->config = $config;
}
public function handle(Event $event): void {
if (!($event instanceof PostLoginEvent)) {
// Unrelated
return;
}
$userId = $event->getUser()->getUID();
try {
$this->settingsMapper->getSettingsByUser($userId);
} catch (DoesNotExistException $e) {
$defaultSoundNotification = $this->config->getAppValue(Application::APP_ID, 'sound_notification') === 'yes' ? 'yes' : 'no';
$defaultSoundTalk = $this->config->getAppValue(Application::APP_ID, 'sound_talk') === 'yes' ? 'yes' : 'no';
$defaultBatchtime = (int) $this->config->getAppValue(Application::APP_ID, 'setting_batchtime');
if ($defaultBatchtime !== Settings::EMAIL_SEND_WEEKLY
&& $defaultBatchtime !== Settings::EMAIL_SEND_DAILY
&& $defaultBatchtime !== Settings::EMAIL_SEND_3HOURLY
&& $defaultBatchtime !== Settings::EMAIL_SEND_HOURLY
&& $defaultBatchtime !== Settings::EMAIL_SEND_OFF) {
$defaultBatchtime = Settings::EMAIL_SEND_3HOURLY;
}
$this->config->setUserValue($userId, Application::APP_ID, 'sound_notification', $defaultSoundNotification);
$this->config->setUserValue($userId, Application::APP_ID, 'sound_talk', $defaultSoundTalk);
$this->settingsMapper->setBatchSettingForUser($userId, $defaultBatchtime);
}
}
}
@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2022, Nikita Toponen <natoponen@gmail.com>
*
* @author Nikita Toponen <natoponen@gmail.com>
*
* @license AGPL-3.0-or-later
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Notifications\Listener;
use OCA\Notifications\AppInfo\Application;
use OCA\Notifications\Model\Settings;
use OCA\Notifications\Model\SettingsMapper;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\IConfig;
use OCP\User\Events\UserCreatedEvent;
/**
* @template-implements IEventListener<Event|UserCreatedEvent>
*/
class UserCreatedListener implements IEventListener {
private SettingsMapper $settingsMapper;
private IConfig $config;
public function __construct(SettingsMapper $settingsMapper, IConfig $config) {
$this->settingsMapper = $settingsMapper;
$this->config = $config;
}
public function handle(Event $event): void {
if (!($event instanceof UserCreatedEvent)) {
// Unrelated
return;
}
$userId = $event->getUser()->getUID();
$defaultSoundNotification = $this->config->getAppValue(Application::APP_ID, 'sound_notification') === 'yes' ? 'yes' : 'no';
$defaultSoundTalk = $this->config->getAppValue(Application::APP_ID, 'sound_talk') === 'yes' ? 'yes' : 'no';
$defaultBatchtime = (int) $this->config->getAppValue(Application::APP_ID, 'setting_batchtime');
if ($defaultBatchtime !== Settings::EMAIL_SEND_WEEKLY
&& $defaultBatchtime !== Settings::EMAIL_SEND_DAILY
&& $defaultBatchtime !== Settings::EMAIL_SEND_3HOURLY
&& $defaultBatchtime !== Settings::EMAIL_SEND_HOURLY
&& $defaultBatchtime !== Settings::EMAIL_SEND_OFF) {
$defaultBatchtime = Settings::EMAIL_SEND_3HOURLY;
}
$this->config->setUserValue($userId, Application::APP_ID, 'sound_notification', $defaultSoundNotification);
$this->config->setUserValue($userId, Application::APP_ID, 'sound_talk', $defaultSoundTalk);
$this->settingsMapper->setBatchSettingForUser($userId, $defaultBatchtime);
}
}
@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2020, Daniel Kesselberg <mail@danielkesselberg.de>
*
* @author Joas Schilling <coding@schilljs.com>
* @author Daniel Kesselberg <mail@danielkesselberg.de>
*
* @license AGPL-3.0-or-later
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Notifications\Listener;
use OCA\Notifications\Handler;
use OCA\Notifications\Model\SettingsMapper;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\User\Events\UserDeletedEvent;
/**
* @template-implements IEventListener<Event|UserDeletedEvent>
*/
class UserDeletedListener implements IEventListener {
private Handler $handler;
private SettingsMapper $settingsMapper;
public function __construct(
Handler $handler,
SettingsMapper $settingsMapper,
) {
$this->handler = $handler;
$this->settingsMapper = $settingsMapper;
}
public function handle(Event $event): void {
if (!($event instanceof UserDeletedEvent)) {
// Unrelated
return;
}
$user = $event->getUser();
$this->handler->deleteByUser($user->getUID());
$this->settingsMapper->deleteSettingsByUser($user->getUID());
}
}
@@ -0,0 +1,394 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2021, Julien Barnoin <julien@barnoin.com>
*
* @author Julien Barnoin <julien@barnoin.com>
*
* @license AGPL-3.0-or-later
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Notifications;
use OCA\Notifications\Model\Settings;
use OCA\Notifications\Model\SettingsMapper;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Defaults;
use OCP\IConfig;
use OCP\IDateTimeFormatter;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserManager;
use OCP\L10N\IFactory;
use OCP\Mail\IMailer;
use OCP\Mail\IMessage;
use OCP\Notification\IAction;
use OCP\Notification\IManager;
use OCP\Notification\INotification;
use OCP\Util;
use Psr\Log\LoggerInterface;
class MailNotifications {
/** @var IConfig */
private $config;
/** @var IManager */
private $manager;
/** @var Handler */
protected $handler;
/** @var IUserManager */
private $userManager;
/** @var LoggerInterface */
private $logger;
/** @var IMailer */
private $mailer;
/** @var IURLGenerator */
private $urlGenerator;
/** @var Defaults */
private $defaults;
/** @var IFactory */
private $l10nFactory;
/** @var IDateTimeFormatter */
private $dateFormatter;
/** @var ITimeFactory */
protected $timeFactory;
/** @var SettingsMapper */
protected $settingsMapper;
public const BATCH_SIZE_CLI = 500;
public const BATCH_SIZE_WEB = 25;
public function __construct(
IConfig $config,
IManager $manager,
Handler $handler,
IUserManager $userManager,
LoggerInterface $logger,
IMailer $mailer,
IURLGenerator $urlGenerator,
Defaults $defaults,
IFactory $l10nFactory,
IDateTimeFormatter $dateTimeFormatter,
ITimeFactory $timeFactory,
SettingsMapper $settingsMapper
) {
$this->config = $config;
$this->manager = $manager;
$this->handler = $handler;
$this->userManager = $userManager;
$this->logger = $logger;
$this->mailer = $mailer;
$this->urlGenerator = $urlGenerator;
$this->defaults = $defaults;
$this->l10nFactory = $l10nFactory;
$this->dateFormatter = $dateTimeFormatter;
$this->timeFactory = $timeFactory;
$this->settingsMapper = $settingsMapper;
}
/**
* Send all due notification emails.
*
* @param int $batchSize
* @param int $sendTime
*/
public function sendEmails(int $batchSize, int $sendTime): void {
$userSettings = $this->settingsMapper->getUsersByNextSendTime($batchSize);
if (empty($userSettings)) {
return;
}
$userIds = array_map(static function (Settings $settings) {
return $settings->getUserId();
}, $userSettings);
// Batch-read settings
$fallbackTimeZone = date_default_timezone_get();
$userTimezones = $this->config->getUserValueForUsers('core', 'timezone', $userIds);
$userEnabled = $this->config->getUserValueForUsers('core', 'enabled', $userIds);
$fallbackLang = $this->config->getSystemValue('force_language', null);
if (is_string($fallbackLang)) {
/** @psalm-var array<string, string> $userLanguages */
$userLanguages = [];
} else {
$fallbackLang = $this->config->getSystemValueString('default_language', 'en');
/** @psalm-var array<string, string> $userLanguages */
$userLanguages = $this->config->getUserValueForUsers('core', 'lang', $userIds);
}
foreach ($userSettings as $settings) {
if (isset($userEnabled[$settings->getUserId()]) && $userEnabled[$settings->getUserId()] === 'false') {
// User is disabled, skip sending the email for them
if ($settings->getNextSendTime() <= $sendTime) {
$settings->setNextSendTime(
$sendTime + $settings->getBatchTime()
);
$this->settingsMapper->update($settings);
}
continue;
}
// Get the settings for this particular user, then check if we have notifications to email them
$languageCode = $userLanguages[$settings->getUserId()] ?? $fallbackLang;
$timezone = $userTimezones[$settings->getUserId()] ?? $fallbackTimeZone;
/** @var INotification[] $notifications */
$notifications = $this->handler->getAfterId($settings->getLastSendId(), $settings->getUserId());
if (!empty($notifications)) {
$oldestNotification = end($notifications);
$shouldSendAfter = $oldestNotification->getDateTime()->getTimestamp() + $settings->getBatchTime();
if ($shouldSendAfter <= $sendTime) {
// User has notifications that should send
$this->sendEmailToUser($settings, $notifications, $languageCode, $timezone);
} else {
// User has notifications but we didn't reach the timeout yet,
// So delay sending to the time of the notification + batch setting
$settings->setNextSendTime($shouldSendAfter);
$this->settingsMapper->update($settings);
}
} else {
$settings->setNextSendTime($sendTime + $settings->getBatchTime());
$this->settingsMapper->update($settings);
}
}
}
/**
* send an email to the user containing given list of notifications
*
* @param Settings $settings
* @param INotification[] $notifications
* @param string $language
* @param string $timezone
*/
protected function sendEmailToUser(Settings $settings, array $notifications, string $language, string $timezone): void {
$lastSendId = array_key_first($notifications);
$lastSendTime = $this->timeFactory->getTime();
$preparedNotifications = [];
foreach ($notifications as $notification) {
/** @var INotification $preparedNotification */
try {
$preparedNotification = $this->manager->prepare($notification, $language);
} catch (\InvalidArgumentException $e) {
// The app was disabled, skip the notification
continue;
} catch (\Exception $e) {
$this->logger->error($e->getMessage(), [
'exception' => $e,
]);
continue;
}
$preparedNotifications[] = $preparedNotification;
}
if (count($preparedNotifications) > 0) {
$message = $this->prepareEmailMessage($settings->getUserId(), $preparedNotifications, $language, $timezone);
if ($message !== null) {
try {
$this->mailer->send($message);
} catch (\Exception $e) {
$this->logger->error($e->getMessage(), [
'exception' => $e,
]);
return;
}
$settings->setLastSendId($lastSendId);
$settings->setNextSendTime($lastSendTime + $settings->getBatchTime());
$this->settingsMapper->update($settings);
}
}
}
/**
* prepare the contents of the email message containing the provided list of notifications
*
* @param string $uid
* @param INotification[] $notifications
* @param string $language
* @param string $timezone
* @return ?IMessage message contents
*/
protected function prepareEmailMessage(string $uid, array $notifications, string $language, string $timezone): ?IMessage {
$user = $this->userManager->get($uid);
if (!$user instanceof IUser) {
return null;
}
$userEmailAddress = $user->getEMailAddress();
if (empty($userEmailAddress)) {
return null;
}
// Prepare our email template
$l10n = $this->l10nFactory->get('notifications', $language);
$template = $this->mailer->createEMailTemplate('notifications.EmailNotification', [
'displayname' => $user->getDisplayName(),
'url' => $this->urlGenerator->getAbsoluteURL('/')
]);
// Prepare email header
$template->addHeader();
$template->addHeading($l10n->t('Hello %s', [$user->getDisplayName()]), $l10n->t('Hello %s,', [$user->getDisplayName()]));
// Prepare email subject and body mentioning amount of notifications
$homeLink = '<a href="' . $this->urlGenerator->getAbsoluteURL('/') . '">' . htmlspecialchars($this->defaults->getName()) . '</a>';
$notificationsCount = count($notifications);
$template->setSubject($l10n->n('New notification for %s', '%n new notifications for %s', $notificationsCount, [$this->defaults->getName()]));
$template->addBodyText(
$l10n->n('You have a new notification for %s', 'You have %n new notifications for %s', $notificationsCount, [$homeLink]),
$l10n->n('You have a new notification for %s', 'You have %n new notifications for %s', $notificationsCount, [$this->urlGenerator->getAbsoluteURL('/')])
);
// Prepare email body with the content of missed notifications
// Notifications are assumed to be passed-in in descending order (latest first). Reversing to present chronologically.
$notifications = array_reverse($notifications);
foreach ($notifications as $notification) {
try {
$relativeDateTime = $this->dateFormatter->formatDateTimeRelativeDay($notification->getDateTime(), 'long', 'short', new \DateTimeZone($timezone), $l10n);
$template->addBodyListItem($this->getHTMLContents($notification), $relativeDateTime, $notification->getIcon(), $notification->getParsedSubject());
// Buttons probably were not intended for this, but it works ok enough for showing the idea.
$actions = $notification->getParsedActions();
foreach ($actions as $action) {
if ($action->getRequestType() === IAction::TYPE_WEB) {
$template->addBodyButton($action->getLabel(), $action->getLink());
}
}
} catch (\Throwable $e) {
$this->logger->error(
'An error occurred while preparing a notification ('
. $notification->getApp() . '|' . $notification->getSubject()
. '|' . $notification->getObjectType() . '|' . $notification->getObjectId()
. ') for sending',
['exception' => $e]
);
return null;
}
}
// Prepare email footer
$template->addBodyText(
$l10n->t('You can change the frequency of these emails or disable them in the <a href="%s">settings</a>.', $this->urlGenerator->linkToRouteAbsolute('settings.PersonalSettings.index', ['section' => 'notifications'])),
$l10n->t('You can change the frequency of these emails or disable them in the settings: %s', $this->urlGenerator->linkToRouteAbsolute('settings.PersonalSettings.index', ['section' => 'notifications']))
);
$template->addFooter();
$message = $this->mailer->createMessage();
$message->useTemplate($template);
$message->setTo([$userEmailAddress => $user->getDisplayName()]);
$message->setFrom([Util::getDefaultEmailAddress('no-reply') => $this->defaults->getName()]);
return $message;
}
/**
* return HTML to display this notification
*
* @param INotification $notification
* @return string
*/
protected function getHTMLContents(INotification $notification): string {
$HTMLSubject = $this->getHTMLSubject($notification);
$link = $notification->getLink();
if ($link !== '') {
$HTMLSubject = '<a href="' . $link . '">' . $HTMLSubject . '</a>';
}
return $HTMLSubject . '<br>' . $this->getHTMLMessage($notification);
}
/**
* return HTML to display the subject of this notification
*
* @param INotification $notification
* @return string
*/
protected function getHTMLSubject(INotification $notification): string {
$contentString = htmlspecialchars($notification->getRichSubject());
if ($contentString === '') {
return htmlspecialchars($notification->getParsedSubject());
}
return $this->replaceRichParameters($notification->getRichSubjectParameters(), $contentString);
}
/**
* return HTML to display the message body of this notification
*
* @param INotification $notification
* @return string
*/
protected function getHTMLMessage(INotification $notification): string {
$contentString = htmlspecialchars($notification->getRichMessage());
if ($contentString === '') {
return htmlspecialchars($notification->getParsedMessage());
}
return $this->replaceRichParameters($notification->getRichMessageParameters(), $contentString);
}
/**
* replace the given parameters in the input content string for display in an email
*
* @param array [string => string] $parameters
* @param string $contentString
* @return string $contentString with parameters processed
*/
protected function replaceRichParameters(array $parameters, string $contentString): string {
$placeholders = $replacements = [];
foreach ($parameters as $placeholder => $parameter) {
$placeholders[] = '{' . $placeholder . '}';
if ($parameter['type'] === 'file') {
$replacement = $parameter['path'];
} else {
$replacement = $parameter['name'];
}
if (isset($parameter['link'])) {
$replacements[] = '<a href="' . $parameter['link'] . '">' . htmlspecialchars($replacement) . '</a>';
} else {
$replacements[] = '<strong>' . htmlspecialchars($replacement) . '</strong>';
}
}
return str_replace($placeholders, $replacements, $contentString);
}
}
@@ -0,0 +1,110 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2019, Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license AGPL-3.0-or-later
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Notifications\Migration;
use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\DB\Types;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
class Version2004Date20190107135757 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) {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
if (!$schema->hasTable('notifications')) {
$table = $schema->createTable('notifications');
$table->addColumn('notification_id', Types::INTEGER, [
'autoincrement' => true,
'notnull' => true,
'length' => 4,
]);
$table->addColumn('app', Types::STRING, [
'notnull' => true,
'length' => 32,
]);
$table->addColumn('user', Types::STRING, [
'notnull' => true,
'length' => 64,
]);
$table->addColumn('timestamp', Types::INTEGER, [
'notnull' => true,
'length' => 4,
'default' => 0,
]);
$table->addColumn('object_type', Types::STRING, [
'notnull' => true,
'length' => 64,
]);
$table->addColumn('object_id', Types::STRING, [
'notnull' => true,
'length' => 64,
]);
$table->addColumn('subject', Types::STRING, [
'notnull' => true,
'length' => 64,
]);
$table->addColumn('subject_parameters', Types::TEXT, [
'notnull' => false,
]);
$table->addColumn('message', Types::STRING, [
'notnull' => false,
'length' => 64,
]);
$table->addColumn('message_parameters', Types::TEXT, [
'notnull' => false,
]);
$table->addColumn('link', Types::STRING, [
'notnull' => false,
'length' => 4000,
]);
$table->addColumn('icon', Types::STRING, [
'notnull' => false,
'length' => 4000,
]);
$table->addColumn('actions', Types::TEXT, [
'notnull' => false,
]);
$table->setPrimaryKey(['notification_id']);
$table->addIndex(['app'], 'oc_notifications_app');
$table->addIndex(['user'], 'oc_notifications_user');
$table->addIndex(['timestamp'], 'oc_notifications_timestamp');
$table->addIndex(['object_type', 'object_id'], 'oc_notifications_object');
}
// $schema->createTable('notifications_pushtokens') was
// replaced with notifications_pushhash in Version2010Date20210218082811
// and deleted in Version2010Date20210218082855
return $schema;
}
}
@@ -0,0 +1,149 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2021, Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license AGPL-3.0-or-later
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Notifications\Migration;
use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\DB\Types;
use OCP\IDBConnection;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
/**
* Recreate notifications_pushtoken(s) with a primary key for cluster support
*/
class Version2010Date20210218082811 extends SimpleMigrationStep {
/** @var IDBConnection */
protected $connection;
public function __construct(IDBConnection $connection) {
$this->connection = $connection;
}
/**
* @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();
if (!$schema->hasTable('notifications_pushhash')) {
$table = $schema->createTable('notifications_pushhash');
$table->addColumn('id', Types::INTEGER, [
'autoincrement' => true,
'notnull' => true,
'length' => 4,
]);
$table->addColumn('uid', Types::STRING, [
'notnull' => true,
'length' => 64,
]);
$table->addColumn('token', Types::INTEGER, [
'notnull' => true,
'length' => 4,
'default' => 0,
]);
$table->addColumn('deviceidentifier', Types::STRING, [
'notnull' => true,
'length' => 128,
]);
$table->addColumn('devicepublickey', Types::STRING, [
'notnull' => true,
'length' => 512,
]);
$table->addColumn('devicepublickeyhash', Types::STRING, [
'notnull' => true,
'length' => 128,
]);
$table->addColumn('pushtokenhash', Types::STRING, [
'notnull' => true,
'length' => 128,
]);
$table->addColumn('proxyserver', Types::STRING, [
'notnull' => true,
'length' => 256,
]);
$table->addColumn('apptype', Types::STRING, [
'notnull' => true,
'length' => 32,
'default' => 'unknown',
]);
$table->setPrimaryKey(['id']);
$table->addUniqueIndex(['uid', 'token'], 'oc_npushhash_uid');
}
return $schema;
}
/**
* @param IOutput $output
* @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
* @param array $options
*/
public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
if (!$this->connection->tableExists('notifications_pushtokens')) {
return;
}
$insert = $this->connection->getQueryBuilder();
$insert->insert('notifications_pushhash')
->values([
'uid' => $insert->createParameter('uid'),
'token' => $insert->createParameter('token'),
'deviceidentifier' => $insert->createParameter('deviceidentifier'),
'devicepublickey' => $insert->createParameter('devicepublickey'),
'devicepublickeyhash' => $insert->createParameter('devicepublickeyhash'),
'pushtokenhash' => $insert->createParameter('pushtokenhash'),
'proxyserver' => $insert->createParameter('proxyserver'),
'apptype' => $insert->createParameter('apptype'),
]);
$query = $this->connection->getQueryBuilder();
$query->select('*')
->from('notifications_pushtokens');
$result = $query->execute();
while ($row = $result->fetch()) {
$insert
->setParameter('uid', $row['uid'])
->setParameter('token', (int) $row['token'], IQueryBuilder::PARAM_INT)
->setParameter('deviceidentifier', $row['deviceidentifier'])
->setParameter('devicepublickey', $row['devicepublickey'])
->setParameter('devicepublickeyhash', $row['devicepublickeyhash'])
->setParameter('pushtokenhash', $row['pushtokenhash'])
->setParameter('proxyserver', $row['proxyserver'])
->setParameter('apptype', $row['apptype'])
;
$insert->execute();
}
$result->closeCursor();
}
}
@@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2021, Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license AGPL-3.0-or-later
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Notifications\Migration;
use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
/**
* Delete the old notifications_pushtokens after we added notifications_pushhash with a primary key
*/
class Version2010Date20210218082855 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();
if ($schema->hasTable('notifications_pushtokens')) {
$schema->dropTable('notifications_pushtokens');
return $schema;
}
return null;
}
}
@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2021, Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license AGPL-3.0-or-later
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Notifications\Migration;
use Closure;
use Doctrine\DBAL\Types\Types;
use OCP\DB\ISchemaWrapper;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
class Version2011Date20210930134607 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();
if (!$schema->hasTable('notifications_settings')) {
$table = $schema->createTable('notifications_settings');
$table->addColumn('id', Types::BIGINT, [
'autoincrement' => true,
'notnull' => true,
'length' => 4,
]);
$table->addColumn('user_id', Types::STRING, [
'notnull' => true,
'length' => 64,
]);
$table->addColumn('batch_time', Types::INTEGER, [
'default' => 0,
'length' => 4,
]);
$table->addColumn('last_send_id', Types::BIGINT, [
'default' => 0,
]);
$table->addColumn('next_send_time', Types::INTEGER, [
'default' => 0,
'length' => 11,
]);
$table->setPrimaryKey(['id']);
$table->addUniqueIndex(['user_id'], 'notset_user');
$table->addIndex(['next_send_time'], 'notset_nextsend');
return $schema;
}
return null;
}
}
@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2022, Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license AGPL-3.0-or-later
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Notifications\Migration;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
class Version2011Date20220826074907 extends SimpleMigrationStep {
/** @var IDBConnection */
protected $connection;
public function __construct(IDBConnection $connection) {
$this->connection = $connection;
}
public function postSchemaChange(IOutput $output, \Closure $schemaClosure, array $options) {
$query = $this->connection->getQueryBuilder();
// The maximum valid value is NOW + 7 days, but since updating is fixed
// and you only run into the bug at the year 2038, we can also count up 8 days.
$time = time() + 3600 * 24 * 8;
$query->update('notifications_settings')
->set('next_send_time', $query->createNamedParameter(1, IQueryBuilder::PARAM_INT))
->where($query->expr()->gt('next_send_time', $query->createNamedParameter($time, IQueryBuilder::PARAM_INT)));
$count = $query->executeStatement();
if ($count > 0) {
$output->info('Fixed next send of ' . $count . ' disabled users');
}
}
}
@@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2021, Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license AGPL-3.0-or-later
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Notifications\Model;
use OCP\AppFramework\Db\Entity;
/**
*
* @method void setUserId(string $userId)
* @method string getUserId()
* @method void setBatchTime(int $batchTime)
* @method int getBatchTime()
* @method void setLastSendId(int $lastSendId)
* @method int getLastSendId()
* @method void setNextSendTime(int $nextSendTime)
* @method int getNextSendTime()
*/
class Settings extends Entity {
public const EMAIL_SEND_WEEKLY = 4;
public const EMAIL_SEND_DAILY = 3;
public const EMAIL_SEND_3HOURLY = 2;
public const EMAIL_SEND_HOURLY = 1;
public const EMAIL_SEND_OFF = 0;
/** @var string */
protected $userId;
/** @var int */
protected $batchTime;
/** @var int */
protected $lastSendId;
/** @var int */
protected $nextSendTime;
public function __construct() {
$this->addType('userId', 'string');
$this->addType('batchTime', 'int');
$this->addType('lastSendId', 'int');
$this->addType('nextSendTime', 'int');
}
public function asArray(): array {
return [
'id' => $this->getId(),
'user_id' => $this->getUserId(),
'batch_time' => $this->getBatchTime(),
'last_send_id' => $this->getLastSendId(),
'next_send_time' => $this->getNextSendTime(),
];
}
}
@@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2021, Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license AGPL-3.0-or-later
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Notifications\Model;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
use OCP\AppFramework\Db\QBMapper;
use OCP\DB\Exception as DBException;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
/**
* @template-extends QBMapper<Settings>
*
* @method Settings mapRowToEntity(array $row)
* @method Settings findEntity(IQueryBuilder $query)
* @method Settings[] findEntities(IQueryBuilder $query)
*/
class SettingsMapper extends QBMapper {
public function __construct(IDBConnection $db) {
parent::__construct($db, 'notifications_settings', Settings::class);
}
/**
* @param string $userId
* @return Settings
* @throws DBException
* @throws MultipleObjectsReturnedException
* @throws DoesNotExistException
*/
public function getSettingsByUser(string $userId): Settings {
$query = $this->db->getQueryBuilder();
$query->select('*')
->from($this->getTableName())
->where($query->expr()->eq('user_id', $query->createNamedParameter($userId)));
return $this->findEntity($query);
}
/**
* @param string $userId
* @throws DBException
*/
public function deleteSettingsByUser(string $userId): void {
$query = $this->db->getQueryBuilder();
$query->delete($this->getTableName())
->where($query->expr()->eq('user_id', $query->createNamedParameter($userId)));
$query->executeStatement();
}
public function setBatchSettingForUser(string $userId, int $batchSetting): void {
try {
$settings = $this->getSettingsByUser($userId);
} catch (DoesNotExistException $e) {
$settings = new Settings();
$settings->setUserId($userId);
/** @var Settings $settings */
$settings = $this->insert($settings);
}
if ($batchSetting === Settings::EMAIL_SEND_WEEKLY) {
$batchTime = 3600 * 24 * 7;
} elseif ($batchSetting === Settings::EMAIL_SEND_DAILY) {
$batchTime = 3600 * 24;
} elseif ($batchSetting === Settings::EMAIL_SEND_3HOURLY) {
$batchTime = 3600 * 3;
} elseif ($batchSetting === Settings::EMAIL_SEND_HOURLY) {
$batchTime = 3600;
} else {
$batchTime = 0; // Off
}
$settings->setBatchTime($batchTime);
if ($batchTime === 0) {
// When mails are Off, we don't set a "next send time" so it can be
// skipped in the background job.
$settings->setNextSendTime(0);
} else {
// This will automatically heal on the first run of the background job.
// We are just setting it to 1, so it's checked soon in case
// the time is now shorter and should trigger already.
$settings->setNextSendTime(1);
}
$this->update($settings);
}
/**
* @param int $limit
* @return Settings[]
* @throws DBException
*/
public function getUsersByNextSendTime(int $limit): array {
$query = $this->db->getQueryBuilder();
$query->select('*')
->from($this->getTableName())
->where($query->expr()->gt('next_send_time', $query->createNamedParameter(0)))
->orderBy('next_send_time', 'ASC')
->setMaxResults($limit);
return $this->findEntities($query);
}
public function createSettingsFromRow(array $row): Settings {
return $this->mapRowToEntity([
'id' => $row['id'],
'user_id' => (string) $row['user_id'],
'batch_time' => (int) $row['batch_time'],
'last_send_id' => (int) $row['last_send_id'],
'next_send_time' => (int) $row['next_send_time'],
]);
}
}
@@ -0,0 +1,222 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2017, Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license AGPL-3.0-or-later
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Notifications\Notifier;
use OCP\Files\Folder;
use OCP\Files\IRootFolder;
use OCP\IURLGenerator;
use OCP\IUser;
use OCP\IUserManager;
use OCP\L10N\IFactory;
use OCP\Notification\AlreadyProcessedException;
use OCP\Notification\IAction;
use OCP\Notification\INotification;
use OCP\Notification\INotifier;
class AdminNotifications implements INotifier {
/** @var IFactory */
protected $l10nFactory;
/** @var IURLGenerator */
protected $urlGenerator;
/** @var IUserManager */
protected $userManager;
/** @var IRootFolder */
protected $rootFolder;
public function __construct(IFactory $l10nFactory,
IURLGenerator $urlGenerator,
IUserManager $userManager,
IRootFolder $rootFolder) {
$this->l10nFactory = $l10nFactory;
$this->urlGenerator = $urlGenerator;
$this->userManager = $userManager;
$this->rootFolder = $rootFolder;
}
/**
* Identifier of the notifier, only use [a-z0-9_]
*
* @return string
* @since 17.0.0
*/
public function getID(): string {
return 'admin_notifications';
}
/**
* Human-readable name describing the notifier
*
* @return string
* @since 17.0.0
*/
public function getName(): string {
return $this->l10nFactory->get('notifications')->t('Admin notifications');
}
/**
* @param INotification $notification
* @param string $languageCode The code of the language that should be used to prepare the notification
* @return INotification
* @throws \InvalidArgumentException When the notification was not prepared by a notifier
* @throws AlreadyProcessedException When the notification is not needed anymore and should be deleted
*/
public function prepare(INotification $notification, string $languageCode): INotification {
if ($notification->getApp() !== 'admin_notifications' && $notification->getApp() !== 'admin_notification_talk') {
throw new \InvalidArgumentException('Unknown app');
}
switch ($notification->getSubject()) {
case 'dummy':
$subjectParams = $notification->getSubjectParameters();
$numActions = (int) $subjectParams[0];
$user = $this->userManager->get($notification->getUser());
assert($user instanceof IUser);
$userFolder = $this->rootFolder->getUserFolder($user->getUID());
$dirList = $userFolder->getDirectoryListing();
if (empty($dirList)) {
$file1 = $userFolder;
} else {
$file1 = array_pop($dirList);
}
if (empty($dirList)) {
$file2 = $userFolder;
} else {
$file2 = array_shift($dirList);
if ($file2 instanceof Folder) {
$dirList = $file2->getDirectoryListing();
if (!empty($dirList)) {
$file2 = array_shift($dirList);
}
}
}
$path1 = rtrim($file1->getPath(), '/');
if (strpos($path1, '/' . $notification->getUser() . '/files/') === 0) {
// Remove /user/files/...
[,,, $path1] = explode('/', $path1, 4);
}
$path2 = rtrim($file2->getPath(), '/');
if (strpos($path2, '/' . $notification->getUser() . '/files/') === 0) {
// Remove /user/files/...
[,,, $path2] = explode('/', $path2, 4);
}
$loremIpsum = 'User {actor} owns a file {item}';
$loremIpsumLong = 'Lorem {user-2} dolor sit {file-3}, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum.' . "\n" . 'Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.';
$notification->setRichSubject($loremIpsum, [
'actor' => [
'type' => 'user',
'id' => $user->getUID(),
'name' => $user->getDisplayName(),
],
'item' => [
'type' => 'file',
'id' => $file1->getId(),
'name' => $file1->getName(),
'size' => $file1->getSize(),
'path' => $path1,
'link' => $this->urlGenerator->linkToRouteAbsolute('files.viewcontroller.showFile', ['fileid' => $file1->getId()]),
'mimetype' => $file1->getMimetype(),
],
]);
$notification->setRichMessage($loremIpsumLong, [
'user-2' => [
'type' => 'user',
'id' => $user->getUID(),
'name' => $user->getDisplayName(),
],
'file-3' => [
'type' => 'file',
'id' => $file2->getId(),
'name' => $file2->getName(),
'size' => $file2->getSize(),
'path' => $path2,
'link' => $this->urlGenerator->linkToRouteAbsolute('files.viewcontroller.showFile', ['fileid' => $file2->getId()]),
'mimetype' => $file2->getMimetype(),
],
]);
$primary = $notification->createAction();
$primary->setPrimary(true);
$primary->setParsedLabel('3 is prim(e|ary)');
$primary->setLink(
'https://en.wikipedia.org/wiki/3#Mathematics',
IAction::TYPE_WEB
);
$secondary = $notification->createAction();
$secondary->setPrimary(false);
$secondary->setParsedLabel('Get status');
$secondary->setLink(
$this->urlGenerator->getAbsoluteURL('status.php'),
IAction::TYPE_GET
);
$three = $notification->createAction();
$three->setPrimary(false);
$three->setParsedLabel('Delete status.php');
$three->setLink(
$this->urlGenerator->getAbsoluteURL('status.php'),
IAction::TYPE_DELETE
);
$numActions = min(3, $numActions);
switch ($numActions) {
case 3:
$notification->addParsedAction($three);
// no break
case 2:
$notification->addParsedAction($secondary);
// no break
case 1:
$notification->addParsedAction($primary);
}
$notification->setIcon($this->urlGenerator->getAbsoluteURL($this->urlGenerator->imagePath('notifications', 'notifications-dark.svg')));
return $notification;
// Deal with known subjects
case 'cli':
case 'ocs':
$subjectParams = $notification->getSubjectParameters();
$notification->setParsedSubject($subjectParams[0]);
$messageParams = $notification->getMessageParameters();
if (isset($messageParams[0]) && $messageParams[0] !== '') {
$notification->setParsedMessage($messageParams[0]);
}
$notification->setIcon($this->urlGenerator->getAbsoluteURL($this->urlGenerator->imagePath('notifications', 'notifications-dark.svg')));
return $notification;
default:
throw new \InvalidArgumentException('Unknown subject');
}
}
}
@@ -0,0 +1,784 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2017, Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license AGPL-3.0-or-later
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Notifications;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\ServerException;
use OC\Authentication\Token\IProvider;
use OC\Security\IdentityProof\Key;
use OC\Security\IdentityProof\Manager;
use OCA\Notifications\AppInfo\Application;
use OCP\AppFramework\Http;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\Authentication\Exceptions\InvalidTokenException;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\Http\Client\IClientService;
use OCP\ICache;
use OCP\ICacheFactory;
use OCP\IConfig;
use OCP\IDBConnection;
use OCP\IUser;
use OCP\L10N\IFactory;
use OCP\Notification\IManager as INotificationManager;
use OCP\Notification\INotification;
use OCP\UserStatus\IManager as IUserStatusManager;
use OCP\UserStatus\IUserStatus;
use OCP\Util;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Output\OutputInterface;
class Push {
/** @var IDBConnection */
protected $db;
/** @var INotificationManager */
protected $notificationManager;
/** @var IConfig */
protected $config;
/** @var IProvider */
protected $tokenProvider;
/** @var Manager */
private $keyManager;
/** @var IClientService */
protected $clientService;
/** @var ICache */
protected $cache;
/** @var IUserStatusManager */
protected $userStatusManager;
/** @var IFactory */
protected $l10nFactory;
/** @var LoggerInterface */
protected $log;
/** @var OutputInterface */
protected $output;
/**
* @var array
* @psalm-var array<string, list<string>>
*/
protected $payloadsToSend = [];
/** @var bool */
protected $deferPreparing = false;
/** @var bool */
protected $deferPayloads = false;
/**
* @var array[] $userId => $appId => $notificationIds
* @psalm-var array<string|int, array<string, list<int>>>
*/
protected $deletesToPush = [];
/**
* @var bool[] $userId => true
* @psalm-var array<string|int, bool>
*/
protected $deleteAllsToPush = [];
/** @var INotification[] */
protected $notificationsToPush = [];
/**
* @var ?IUserStatus[]
* @psalm-var array<string, ?IUserStatus>
*/
protected $userStatuses = [];
/**
* @var array[]
* @psalm-var array<string, list<array{id: int, uid: string, token: int, deviceidentifier: string, devicepublickey: string, devicepublickeyhash: string, pushtokenhash: string, proxyserver: string, apptype: string}>>
*/
protected $userDevices = [];
/** @var string[] */
protected $loadDevicesForUsers = [];
/** @var string[] */
protected $loadStatusForUsers = [];
/**
* A very small and privileged list of apps that are allowed to push during DND.
* @var bool[]
*/
protected $allowedDNDPushList = [
'twofactor_nextcloud_notification' => true,
];
public function __construct(
IDBConnection $connection,
INotificationManager $notificationManager,
IConfig $config,
IProvider $tokenProvider,
Manager $keyManager,
IClientService $clientService,
ICacheFactory $cacheFactory,
IUserStatusManager $userStatusManager,
IFactory $l10nFactory,
protected ITimeFactory $timeFactory,
LoggerInterface $log,
) {
$this->db = $connection;
$this->notificationManager = $notificationManager;
$this->config = $config;
$this->tokenProvider = $tokenProvider;
$this->keyManager = $keyManager;
$this->clientService = $clientService;
$this->cache = $cacheFactory->createDistributed('pushtokens');
$this->userStatusManager = $userStatusManager;
$this->l10nFactory = $l10nFactory;
$this->log = $log;
}
public function setOutput(OutputInterface $output): void {
$this->output = $output;
}
protected function printInfo(string $message): void {
if ($this->output) {
$this->output->writeln($message);
}
}
public function isDeferring(): bool {
return $this->deferPayloads;
}
public function deferPayloads(): void {
$this->deferPreparing = true;
$this->deferPayloads = true;
}
public function flushPayloads(): void {
$this->deferPreparing = false;
if (!empty($this->loadDevicesForUsers)) {
$this->loadDevicesForUsers = array_unique($this->loadDevicesForUsers);
$missingDevicesFor = array_diff($this->loadDevicesForUsers, array_keys($this->userDevices));
$newUserDevices = $this->getDevicesForUsers($missingDevicesFor);
foreach ($missingDevicesFor as $userId) {
$this->userDevices[$userId] = $newUserDevices[$userId] ?? [];
}
$this->loadDevicesForUsers = [];
}
if (!empty($this->loadStatusForUsers)) {
$this->loadStatusForUsers = array_unique($this->loadStatusForUsers);
$missingStatusFor = array_diff($this->loadStatusForUsers, array_keys($this->userStatuses));
$newUserStatuses = $this->userStatusManager->getUserStatuses($missingStatusFor);
foreach ($missingStatusFor as $userId) {
$this->userStatuses[$userId] = $newUserStatuses[$userId] ?? null;
}
$this->loadStatusForUsers = [];
}
if (!empty($this->notificationsToPush)) {
foreach ($this->notificationsToPush as $id => $notification) {
$this->pushToDevice($id, $notification);
}
$this->notificationsToPush = [];
}
if (!empty($this->deleteAllsToPush)) {
foreach ($this->deleteAllsToPush as $userId => $bool) {
$this->pushDeleteToDevice((string) $userId, null);
}
$this->deleteAllsToPush = [];
}
if (!empty($this->deletesToPush)) {
foreach ($this->deletesToPush as $userId => $data) {
foreach ($data as $client => $notificationIds) {
if ($client === 'talk') {
$this->pushDeleteToDevice((string) $userId, $notificationIds, $client);
} else {
foreach ($notificationIds as $notificationId) {
$this->pushDeleteToDevice((string) $userId, [$notificationId], $client);
}
}
}
}
$this->deletesToPush = [];
}
$this->deferPayloads = false;
$this->sendNotificationsToProxies();
}
/**
* @param array $devices
* @psalm-param $devices list<array{id: int, uid: string, token: int, deviceidentifier: string, devicepublickey: string, devicepublickeyhash: string, pushtokenhash: string, proxyserver: string, apptype: string}>
* @param string $app
* @return array
* @psalm-return list<array{id: int, uid: string, token: int, deviceidentifier: string, devicepublickey: string, devicepublickeyhash: string, pushtokenhash: string, proxyserver: string, apptype: string}>
*/
public function filterDeviceList(array $devices, string $app): array {
$isTalkNotification = \in_array($app, ['spreed', 'talk', 'admin_notification_talk'], true);
$talkDevices = array_filter($devices, static function ($device) {
return $device['apptype'] === 'talk';
});
$otherDevices = array_filter($devices, static function ($device) {
return $device['apptype'] !== 'talk';
});
$this->printInfo('Identified ' . count($talkDevices) . ' Talk devices and ' . count($otherDevices) . ' others.');
if (!$isTalkNotification) {
if (empty($otherDevices)) {
// We only send file notifications to the files app.
// If you don't have such a device, bye!
return [];
}
return $otherDevices;
}
if (empty($talkDevices)) {
// If you don't have a talk device,
// we fall back to the files app.
return $otherDevices;
}
return $talkDevices;
}
public function pushToDevice(int $id, INotification $notification, ?OutputInterface $output = null): void {
if (!$this->config->getSystemValueBool('has_internet_connection', true)) {
return;
}
if ($this->deferPreparing) {
$this->notificationsToPush[$id] = clone $notification;
$this->loadDevicesForUsers[] = $notification->getUser();
$this->loadStatusForUsers[] = $notification->getUser();
return;
}
$user = $this->createFakeUserObject($notification->getUser());
if (!array_key_exists($notification->getUser(), $this->userStatuses)) {
$userStatus = $this->userStatusManager->getUserStatuses([
$notification->getUser(),
]);
$this->userStatuses[$notification->getUser()] = $userStatus[$notification->getUser()] ?? null;
}
if (isset($this->userStatuses[$notification->getUser()])) {
$userStatus = $this->userStatuses[$notification->getUser()];
if ($userStatus->getStatus() === IUserStatus::DND && empty($this->allowedDNDPushList[$notification->getApp()])) {
$this->printInfo('<error>User status is set to DND - no push notifications will be sent</error>');
return;
}
}
if (!array_key_exists($notification->getUser(), $this->userDevices)) {
$devices = $this->getDevicesForUser($notification->getUser());
$this->userDevices[$notification->getUser()] = $devices;
} else {
$devices = $this->userDevices[$notification->getUser()];
}
if (empty($devices)) {
$this->printInfo('No devices found for user');
return;
}
$this->printInfo('Trying to push to ' . count($devices) . ' devices');
$this->printInfo('');
$language = $this->l10nFactory->getUserLanguage($user);
$this->printInfo('Language is set to ' . $language);
if (!$notification->isValidParsed()) {
try {
$this->notificationManager->setPreparingPushNotification(true);
$notification = $this->notificationManager->prepare($notification, $language);
} catch (\InvalidArgumentException $e) {
return;
} finally {
$this->notificationManager->setPreparingPushNotification(false);
}
}
$userKey = $this->keyManager->getKey($user);
$this->printInfo('Private user key size: ' . strlen($userKey->getPrivate()));
$this->printInfo('Public user key size: ' . strlen($userKey->getPublic()));
$isTalkNotification = \in_array($notification->getApp(), ['spreed', 'talk', 'admin_notification_talk'], true);
$devices = $this->filterDeviceList($devices, $notification->getApp());
if (empty($devices)) {
return;
}
// We don't push to devices that are older than 60 days
$maxAge = time() - 60 * 24 * 60 * 60;
foreach ($devices as $device) {
$device['token'] = (int) $device['token'];
$this->printInfo('');
$this->printInfo('Device token:' . $device['token']);
if (!$this->validateToken($device['token'], $maxAge)) {
// Token does not exist anymore
continue;
}
try {
$payload = json_encode($this->encryptAndSign($userKey, $device, $id, $notification, $isTalkNotification), JSON_THROW_ON_ERROR);
$proxyServer = rtrim($device['proxyserver'], '/');
if (!isset($this->payloadsToSend[$proxyServer])) {
$this->payloadsToSend[$proxyServer] = [];
}
$this->payloadsToSend[$proxyServer][] = $payload;
} catch (\JsonException $e) {
$this->log->error('JSON error while encoding push notification: ' . $e->getMessage(), ['exception' => $e]);
} catch (\InvalidArgumentException $e) {
// Failed to encrypt message for device: public key is invalid
$this->deletePushToken($device['token']);
}
}
if (!$this->deferPayloads) {
$this->sendNotificationsToProxies();
}
}
/**
* @param string $userId
* @param ?int[] $notificationIds
* @param string $app
*/
public function pushDeleteToDevice(string $userId, ?array $notificationIds, string $app = ''): void {
if (!$this->config->getSystemValueBool('has_internet_connection', true)) {
return;
}
if ($this->deferPreparing) {
if ($notificationIds === null) {
$this->deleteAllsToPush[$userId] = true;
if (isset($this->deletesToPush[$userId])) {
unset($this->deletesToPush[$userId]);
}
} else {
if (isset($this->deleteAllsToPush[$userId])) {
return;
}
$isTalkNotification = \in_array($app, ['spreed', 'talk', 'admin_notification_talk'], true);
$clientGroup = $isTalkNotification ? 'talk' : 'files';
if (!isset($this->deletesToPush[$userId])) {
$this->deletesToPush[$userId] = [];
}
if (!isset($this->deletesToPush[$userId][$clientGroup])) {
$this->deletesToPush[$userId][$clientGroup] = [];
}
foreach ($notificationIds as $notificationId) {
$this->deletesToPush[$userId][$clientGroup][] = $notificationId;
}
}
$this->loadDevicesForUsers[] = $userId;
return;
}
$deleteAll = $notificationIds === null;
$user = $this->createFakeUserObject($userId);
if (!array_key_exists($userId, $this->userDevices)) {
$devices = $this->getDevicesForUser($userId);
$this->userDevices[$userId] = $devices;
} else {
$devices = $this->userDevices[$userId];
}
if (!$deleteAll) {
// Only filter when it's not delete-all
$devices = $this->filterDeviceList($devices, $app);
}
if (empty($devices)) {
return;
}
// We don't push to devices that are older than 60 days
$maxAge = time() - 60 * 24 * 60 * 60;
$userKey = $this->keyManager->getKey($user);
foreach ($devices as $device) {
$device['token'] = (int) $device['token'];
if (!$this->validateToken($device['token'], $maxAge)) {
// Token does not exist anymore
continue;
}
try {
$proxyServer = rtrim($device['proxyserver'], '/');
if (!isset($this->payloadsToSend[$proxyServer])) {
$this->payloadsToSend[$proxyServer] = [];
}
if ($deleteAll) {
$data = $this->encryptAndSignDelete($userKey, $device, null);
try {
$this->payloadsToSend[$proxyServer][] = json_encode($data['payload'], JSON_THROW_ON_ERROR);
} catch (\JsonException $e) {
$this->log->error('JSON error while encoding push notification: ' . $e->getMessage(), ['exception' => $e]);
}
} else {
$temp = $notificationIds;
while (!empty($temp)) {
$data = $this->encryptAndSignDelete($userKey, $device, $temp);
$temp = $data['remaining'];
try {
$this->payloadsToSend[$proxyServer][] = json_encode($data['payload'], JSON_THROW_ON_ERROR);
} catch (\JsonException $e) {
$this->log->error('JSON error while encoding push notification: ' . $e->getMessage(), ['exception' => $e]);
}
}
}
} catch (\InvalidArgumentException $e) {
// Failed to encrypt message for device: public key is invalid
$this->deletePushToken($device['token']);
}
}
if (!$this->deferPayloads) {
$this->sendNotificationsToProxies();
}
}
protected function sendNotificationsToProxies(): void {
$pushNotifications = $this->payloadsToSend;
$this->payloadsToSend = [];
if (empty($pushNotifications)) {
return;
}
if (!$this->notificationManager->isFairUseOfFreePushService()) {
/**
* We want to keep offering our push notification service for free, but large
* users overload our infrastructure. For this reason we have to rate-limit the
* use of push notifications. If you need this feature, consider using Nextcloud Enterprise.
*/
return;
}
$client = $this->clientService->newClient();
foreach ($pushNotifications as $proxyServer => $notifications) {
try {
$requestData = [
'body' => [
'notifications' => $notifications,
],
];
if ($proxyServer === 'https://push-notifications.nextcloud.com') {
$subscriptionKey = $this->config->getAppValue('support', 'subscription_key');
if ($subscriptionKey) {
$requestData['headers']['X-Nextcloud-Subscription-Key'] = $subscriptionKey;
}
}
$response = $client->post($proxyServer . '/notifications', $requestData);
$status = $response->getStatusCode();
$body = $response->getBody();
$bodyData = json_decode($body, true);
} catch (ClientException $e) {
// Server responded with 4xx (400 Bad Request mostlikely)
$response = $e->getResponse();
$status = $response->getStatusCode();
$body = $response->getBody()->getContents();
$bodyData = json_decode($body, true);
} catch (ServerException $e) {
// Server responded with 5xx
$response = $e->getResponse();
$body = $response->getBody()->getContents();
$error = \is_string($body) ? $body : ('no reason given (' . $response->getStatusCode() . ')');
$this->log->debug('Could not send notification to push server [{url}]: {error}', [
'error' => $error,
'url' => $proxyServer,
'app' => 'notifications',
]);
$this->printInfo('Could not send notification to push server [' . $proxyServer . ']: ' . $error);
continue;
} catch (\Exception $e) {
$this->log->error($e->getMessage(), [
'exception' => $e,
]);
$error = $e->getMessage() ?: 'no reason given';
$this->printInfo('Could not send notification to push server [' . get_class($e) . ']: ' . $error);
continue;
}
if (is_array($bodyData) && array_key_exists('unknown', $bodyData) && array_key_exists('failed', $bodyData)) {
if (is_array($bodyData['unknown'])) {
// Proxy returns null when the array is empty
foreach ($bodyData['unknown'] as $unknownDevice) {
$this->printInfo('Deleting device because it is unknown by the push server: ' . $unknownDevice);
$this->deletePushTokenByDeviceIdentifier($unknownDevice);
}
}
if ($bodyData['failed'] !== 0) {
$this->printInfo('Push notification sent, but ' . $bodyData['failed'] . ' failed');
} else {
$this->printInfo('Push notification sent successfully');
}
} elseif ($status !== Http::STATUS_OK) {
if ($status === Http::STATUS_TOO_MANY_REQUESTS) {
$this->config->setAppValue(Application::APP_ID, 'rate_limit_reached', (string) $this->timeFactory->getTime());
}
$error = $body && $bodyData === null ? $body : 'no reason given';
$this->printInfo('Could not send notification to push server [' . $proxyServer . ']: ' . $error);
$this->log->warning('Could not send notification to push server [{url}]: {error}', [
'error' => $error,
'url' => $proxyServer,
'app' => 'notifications',
]);
} else {
$error = $body && $bodyData === null ? $body : 'no reason given';
$this->printInfo('Push notification sent but response was not parsable, using an outdated push proxy? [' . $proxyServer . ']: ' . $error);
$this->log->info('Push notification sent but response was not parsable, using an outdated push proxy? [{url}]: {error}', [
'error' => $error,
'url' => $proxyServer,
'app' => 'notifications',
]);
}
}
}
protected function validateToken(int $tokenId, int $maxAge): bool {
$age = $this->cache->get('t' . $tokenId);
if ($age !== null) {
return $age > $maxAge;
}
try {
// Check if the token is still valid...
$token = $this->tokenProvider->getTokenById($tokenId);
$this->cache->set('t' . $tokenId, $token->getLastCheck(), 600);
if ($token->getLastCheck() > $maxAge) {
$this->printInfo('Device token is valid');
} else {
$this->printInfo('Device token "last checked" is older than 60 days: ' . $token->getLastCheck());
}
return $token->getLastCheck() > $maxAge;
} catch (InvalidTokenException $e) {
// Token does not exist anymore, should drop the push device entry
$this->printInfo('InvalidTokenException is thrown');
$this->deletePushToken($tokenId);
$this->cache->set('t' . $tokenId, 0, 600);
return false;
}
}
/**
* @param Key $userKey
* @param array $device
* @param int $id
* @param INotification $notification
* @param bool $isTalkNotification
* @return array
* @psalm-return array{deviceIdentifier: string, pushTokenHash: string, subject: string, signature: string, priority: string, type: string}
* @throws InvalidTokenException
* @throws \InvalidArgumentException
*/
protected function encryptAndSign(Key $userKey, array $device, int $id, INotification $notification, bool $isTalkNotification): array {
$data = [
'nid' => $id,
'app' => $notification->getApp(),
'subject' => '',
'type' => $notification->getObjectType(),
'id' => $notification->getObjectId(),
];
// Max length of encryption is ~240, so we need to make sure the subject is shorter.
// Also, subtract two for encapsulating quotes will be added.
$maxDataLength = 200 - strlen(json_encode($data)) - 2;
$data['subject'] = Util::shortenMultibyteString($notification->getParsedSubject(), $maxDataLength);
if ($notification->getParsedSubject() !== $data['subject']) {
$data['subject'] .= '…';
}
if ($isTalkNotification) {
$priority = 'high';
$type = $data['type'] === 'call' ? 'voip' : 'alert';
} elseif ($data['app'] === 'twofactor_nextcloud_notification' || $data['app'] === 'phonetrack') {
$priority = 'high';
$type = 'alert';
} else {
$priority = 'normal';
$type = 'alert';
}
$this->printInfo('Device public key size: ' . strlen($device['devicepublickey']));
$this->printInfo('Data to encrypt is: ' . json_encode($data));
if (!openssl_public_encrypt(json_encode($data), $encryptedSubject, $device['devicepublickey'], OPENSSL_PKCS1_PADDING)) {
$error = openssl_error_string();
$this->log->error($error, ['app' => 'notifications']);
$this->printInfo('Error while encrypting data: "' . $error . '"');
throw new \InvalidArgumentException('Failed to encrypt message for device');
}
if (openssl_sign($encryptedSubject, $signature, $userKey->getPrivate(), OPENSSL_ALGO_SHA512)) {
$this->printInfo('Signed encrypted push subject');
} else {
$this->printInfo('Failed to signed encrypted push subject');
}
$base64EncryptedSubject = base64_encode($encryptedSubject);
$base64Signature = base64_encode($signature);
return [
'deviceIdentifier' => $device['deviceidentifier'],
'pushTokenHash' => $device['pushtokenhash'],
'subject' => $base64EncryptedSubject,
'signature' => $base64Signature,
'priority' => $priority,
'type' => $type,
];
}
/**
* @param Key $userKey
* @param array $device
* @param ?int[] $ids
* @return array
* @psalm-return array{remaining: list<int>, payload: array{deviceIdentifier: string, pushTokenHash: string, subject: string, signature: string, priority: string, type: string}}
* @throws InvalidTokenException
* @throws \InvalidArgumentException
*/
protected function encryptAndSignDelete(Key $userKey, array $device, ?array $ids): array {
$remainingIds = [];
if ($ids === null) {
$data = [
'delete-all' => true,
];
} elseif (count($ids) === 1) {
$data = [
'nid' => array_pop($ids),
'delete' => true,
];
} else {
$remainingIds = array_splice($ids, 10);
$data = [
'nids' => $ids,
'delete-multiple' => true,
];
}
if (!openssl_public_encrypt(json_encode($data), $encryptedSubject, $device['devicepublickey'], OPENSSL_PKCS1_PADDING)) {
$this->log->error(openssl_error_string(), ['app' => 'notifications']);
throw new \InvalidArgumentException('Failed to encrypt message for device');
}
openssl_sign($encryptedSubject, $signature, $userKey->getPrivate(), OPENSSL_ALGO_SHA512);
$base64EncryptedSubject = base64_encode($encryptedSubject);
$base64Signature = base64_encode($signature);
return [
'remaining' => $remainingIds,
'payload' => [
'deviceIdentifier' => $device['deviceidentifier'],
'pushTokenHash' => $device['pushtokenhash'],
'subject' => $base64EncryptedSubject,
'signature' => $base64Signature,
'priority' => 'normal',
'type' => 'background',
]
];
}
/**
* @param string $uid
* @return array[]
* @psalm-return list<array{id: int, uid: string, token: int, deviceidentifier: string, devicepublickey: string, devicepublickeyhash: string, pushtokenhash: string, proxyserver: string, apptype: string}>
*/
protected function getDevicesForUser(string $uid): array {
$query = $this->db->getQueryBuilder();
$query->select('*')
->from('notifications_pushhash')
->where($query->expr()->eq('uid', $query->createNamedParameter($uid)));
$result = $query->executeQuery();
$devices = $result->fetchAll();
$result->closeCursor();
return $devices;
}
/**
* @param string[] $userIds
* @return array[]
* @psalm-return array<string, list<array{id: int, uid: string, token: int, deviceidentifier: string, devicepublickey: string, devicepublickeyhash: string, pushtokenhash: string, proxyserver: string, apptype: string}>>
*/
protected function getDevicesForUsers(array $userIds): array {
$query = $this->db->getQueryBuilder();
$query->select('*')
->from('notifications_pushhash')
->where($query->expr()->in('uid', $query->createNamedParameter($userIds, IQueryBuilder::PARAM_STR_ARRAY)));
$devices = [];
$result = $query->executeQuery();
while ($row = $result->fetch()) {
if (!isset($devices[$row['uid']])) {
$devices[$row['uid']] = [];
}
$devices[$row['uid']][] = $row;
}
$result->closeCursor();
return $devices;
}
/**
* @param int $tokenId
* @return bool
*/
protected function deletePushToken(int $tokenId): bool {
$query = $this->db->getQueryBuilder();
$query->delete('notifications_pushhash')
->where($query->expr()->eq('token', $query->createNamedParameter($tokenId, IQueryBuilder::PARAM_INT)));
return $query->executeStatement() !== 0;
}
/**
* @param string $deviceIdentifier
* @return bool
*/
protected function deletePushTokenByDeviceIdentifier(string $deviceIdentifier): bool {
$query = $this->db->getQueryBuilder();
$query->delete('notifications_pushhash')
->where($query->expr()->eq('deviceidentifier', $query->createNamedParameter($deviceIdentifier)));
return $query->executeStatement() !== 0;
}
protected function createFakeUserObject(string $userId): IUser {
return new FakeUser($userId);
}
}
@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Kate Döen <kate.doeen@nextcloud.com>
*
* @author Kate Döen <kate.doeen@nextcloud.com>
*
* @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 <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Notifications;
/**
* @psalm-type NotificationsNotificationAction = array{
* label: string,
* link: string,
* type: string,
* primary: bool,
* }
*
* @psalm-type NotificationsNotification = array{
* notification_id: int,
* app: string,
* user: string,
* datetime: string,
* object_type: string,
* object_id: string,
* subject: string,
* message: string,
* link: string,
* actions: NotificationsNotificationAction[],
* subjectRich?: string,
* subjectRichParameters?: array<string, mixed>,
* messageRich?: string,
* messageRichParameters?: array<string, mixed>,
* icon?: string,
* shouldNotify?: bool,
* }
*
* @psalm-type NotificationsPushDevice = array{
* publicKey: string,
* deviceIdentifier: string,
* signature: string,
* }
*/
class ResponseDefinitions {
}
@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023, Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @license AGPL-3.0-or-later
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Notifications\Service;
use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\IDBConnection;
use OCP\IRequest;
class ClientService {
public const DESKTOP_CLIENT_TIMEOUT = 120;
public function __construct(
protected IDBConnection $connection,
protected IRequest $request,
) {
}
public function hasTalkDesktop(string $userId, int $maxAge = 0): bool {
$query = $this->connection->getQueryBuilder();
$query->select('name')
->from('authtoken')
->where($query->expr()->eq('uid', $query->createNamedParameter($userId)));
if ($maxAge !== 0) {
$query->andWhere($query->expr()->gte(
'last_activity',
$query->createNamedParameter($maxAge, IQueryBuilder::PARAM_INT)
));
}
$result = $query->executeQuery();
while ($row = $result->fetch()) {
if (preg_match('/ \(Talk Desktop Client - [A-Za-z ]+\)$/', $row['name'])) {
$result->closeCursor();
return true;
}
}
$result->closeCursor();
return false;
}
}
@@ -0,0 +1,101 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2022, Nikita Toponen <natoponen@gmail.com>
*
* @author Nikita Toponen <natoponen@gmail.com>
*
* @license AGPL-3.0-or-later
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Notifications\Settings;
use OCA\Notifications\AppInfo\Application;
use OCA\Notifications\Model\Settings;
use OCA\Notifications\Model\SettingsMapper;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IUserSession;
use OCP\Settings\ISettings;
use OCP\Util;
class Admin implements ISettings {
protected IConfig $config;
protected IL10N $l10n;
private SettingsMapper $settingsMapper;
private IUserSession $session;
private IInitialState $initialState;
public function __construct(IConfig $config,
IL10N $l10n,
IUserSession $session,
SettingsMapper $settingsMapper,
IInitialState $initialState) {
$this->config = $config;
$this->l10n = $l10n;
$this->settingsMapper = $settingsMapper;
$this->session = $session;
$this->initialState = $initialState;
}
public function getForm(): TemplateResponse {
Util::addScript('notifications', 'notifications-admin-settings');
$defaultSoundNotification = $this->config->getAppValue(Application::APP_ID, 'sound_notification') === 'yes' ? 'yes' : 'no';
$defaultSoundTalk = $this->config->getAppValue(Application::APP_ID, 'sound_talk') === 'yes' ? 'yes' : 'no';
$defaultBatchtime = (int) $this->config->getAppValue(Application::APP_ID, 'setting_batchtime');
if ($defaultBatchtime != Settings::EMAIL_SEND_WEEKLY
&& $defaultBatchtime != Settings::EMAIL_SEND_DAILY
&& $defaultBatchtime != Settings::EMAIL_SEND_3HOURLY
&& $defaultBatchtime != Settings::EMAIL_SEND_HOURLY
&& $defaultBatchtime != Settings::EMAIL_SEND_OFF) {
$defaultBatchtime = Settings::EMAIL_SEND_3HOURLY;
}
$this->initialState->provideInitialState('config', [
'setting' => 'admin',
'setting_batchtime' => $defaultBatchtime,
'sound_notification' => $defaultSoundNotification === 'yes',
'sound_talk' => $defaultSoundTalk === 'yes',
]);
return new TemplateResponse('notifications', 'settings/admin');
}
/**
* @return string the section ID, e.g. 'sharing'
*/
public function getSection(): string {
return 'notifications';
}
/**
* @return int whether the form should be rather on the top or bottom of
* the admin section. The forms are arranged in ascending order of the
* priority values. It is required to return a value between 0 and 100.
*
* E.g.: 70
*/
public function getPriority(): int {
return 20;
}
}
@@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2022, Nikita Toponen <natoponen@gmail.com>
*
* @author Nikita Toponen <natoponen@gmail.com>
*
* @license AGPL-3.0-or-later
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Notifications\Settings;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\Settings\IIconSection;
class AdminSection implements IIconSection {
private IL10N $l;
private IURLGenerator $url;
public function __construct(IURLGenerator $url, IL10N $l) {
$this->url = $url;
$this->l = $l;
}
/**
* returns the relative path to an 16*16 icon describing the section.
* e.g. '/core/img/places/files.svg'
*
* @returns string
* @since 12
*/
public function getIcon(): string {
return $this->url->imagePath('notifications', 'notifications-dark.svg');
}
/**
* returns the ID of the section. It is supposed to be a lower case string,
* e.g. 'ldap'
*
* @returns string
* @since 9.1
*/
public function getID(): string {
return 'notifications';
}
/**
* returns the translated name as it should be displayed, e.g. 'LDAP / AD
* integration'. Use the L10N service to translate it.
*
* @return string
* @since 9.1
*/
public function getName(): string {
return $this->l->t('Notifications');
}
/**
* @return int whether the form should be rather on the top or bottom of
* the settings navigation. The sections are arranged in ascending order of
* the priority values. It is required to return a value between 0 and 99.
*
* E.g.: 70
* @since 9.1
*/
public function getPriority(): int {
return 55;
}
}
@@ -0,0 +1,130 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2021, Julien Barnoin <julien@barnoin.com>
*
* @author Julien Barnoin <julien@barnoin.com>
*
* @license AGPL-3.0-or-later
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Notifications\Settings;
use OCA\Notifications\AppInfo\Application;
use OCA\Notifications\Model\Settings;
use OCA\Notifications\Model\SettingsMapper;
use OCP\AppFramework\Db\DoesNotExistException;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
use OCP\IConfig;
use OCP\IL10N;
use OCP\IUser;
use OCP\IUserSession;
use OCP\Settings\ISettings;
use OCP\Util;
class Personal implements ISettings {
/** @var \OCP\IConfig */
protected $config;
/** @var \OCP\IL10N */
protected $l10n;
/** @var SettingsMapper */
private $settingsMapper;
/** @var IUserSession */
private $session;
/** @var IInitialState */
private $initialState;
public function __construct(IConfig $config,
IL10N $l10n,
IUserSession $session,
SettingsMapper $settingsMapper,
IInitialState $initialState) {
$this->config = $config;
$this->l10n = $l10n;
$this->settingsMapper = $settingsMapper;
$this->session = $session;
$this->initialState = $initialState;
}
/**
* @return TemplateResponse
*/
public function getForm(): TemplateResponse {
Util::addScript('notifications', 'notifications-settings');
/** @var IUser $user */
$user = $this->session->getUser();
try {
$settings = $this->settingsMapper->getSettingsByUser($user->getUID());
if ($settings->getBatchTime() === 3600 * 24 * 7) {
$settingBatchTime = Settings::EMAIL_SEND_WEEKLY;
} elseif ($settings->getBatchTime() === 3600 * 24) {
$settingBatchTime = Settings::EMAIL_SEND_DAILY;
} elseif ($settings->getBatchTime() === 3600 * 3) {
$settingBatchTime = Settings::EMAIL_SEND_3HOURLY;
} elseif ($settings->getBatchTime() === 3600) {
$settingBatchTime = Settings::EMAIL_SEND_HOURLY;
} else {
$settingBatchTime = Settings::EMAIL_SEND_OFF;
}
} catch (DoesNotExistException $e) {
$settings = new Settings();
$settings->setUserId($user->getUID());
$settings->setBatchTime(3600 * 3);
$settings->setNextSendTime(1);
$this->settingsMapper->insert($settings);
$settingBatchTime = Settings::EMAIL_SEND_3HOURLY;
}
$this->initialState->provideInitialState('config', [
'setting' => 'personal',
'is_email_set' => (bool)$user->getEMailAddress(),
'setting_batchtime' => $settingBatchTime,
'sound_notification' => $this->config->getUserValue($user->getUID(), Application::APP_ID, 'sound_notification', 'yes') === 'yes',
'sound_talk' => $this->config->getUserValue($user->getUID(), Application::APP_ID, 'sound_talk', 'yes') === 'yes',
]);
return new TemplateResponse('notifications', 'settings/personal');
}
/**
* @return string the section ID, e.g. 'sharing'
*/
public function getSection(): string {
return 'notifications';
}
/**
* @return int whether the form should be rather on the top or bottom of
* the admin section. The forms are arranged in ascending order of the
* priority values. It is required to return a value between 0 and 100.
*
* E.g.: 70
*/
public function getPriority(): int {
return 20;
}
}
@@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2021, Julien Barnoin <julien@barnoin.com>
*
* @author Julien Barnoin <julien@barnoin.com>
*
* @license AGPL-3.0-or-later
*
* 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 <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Notifications\Settings;
use OCP\IL10N;
use OCP\IURLGenerator;
use OCP\Settings\IIconSection;
class PersonalSection implements IIconSection {
/** @var IL10N */
private $l;
/** @var IURLGenerator */
private $url;
public function __construct(IURLGenerator $url, IL10N $l) {
$this->url = $url;
$this->l = $l;
}
/**
* returns the relative path to an 16*16 icon describing the section.
* e.g. '/core/img/places/files.svg'
*
* @returns string
* @since 12
*/
public function getIcon(): string {
return $this->url->imagePath('notifications', 'notifications-dark.svg');
}
/**
* returns the ID of the section. It is supposed to be a lower case string,
* e.g. 'ldap'
*
* @returns string
* @since 9.1
*/
public function getID(): string {
return 'notifications';
}
/**
* returns the translated name as it should be displayed, e.g. 'LDAP / AD
* integration'. Use the L10N service to translate it.
*
* @return string
* @since 9.1
*/
public function getName(): string {
return $this->l->t('Notifications');
}
/**
* @return int whether the form should be rather on the top or bottom of
* the settings navigation. The sections are arranged in ascending order of
* the priority values. It is required to return a value between 0 and 99.
*
* E.g.: 70
* @since 9.1
*/
public function getPriority(): int {
return 10;
}
}
@@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
/**
* @copyright Copyright (c) 2023 Joas Schilling <coding@schilljs.com>
*
* @author Joas Schilling <coding@schilljs.com>
*
* @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 <http://www.gnu.org/licenses/>.
*
*/
namespace OCA\Notifications\Settings;
use OCA\Notifications\AppInfo\Application;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IConfig;
use OCP\IL10N;
use OCP\SetupCheck\ISetupCheck;
use OCP\SetupCheck\SetupResult;
class SetupWarningOnRateLimitReached implements ISetupCheck {
public function __construct(
private IConfig $config,
private ITimeFactory $timeFactory,
private IL10N $l,
) {
}
/**
* @inheritDoc
*/
public function getCategory(): string {
return 'notifications';
}
/**
* @inheritDoc
*/
public function getName(): string {
return $this->l->t('Push notifications - Fair use policy');
}
/**
* @inheritDoc
*/
public function run(): SetupResult {
$lastReached = (int) $this->config->getAppValue(Application::APP_ID, 'rate_limit_reached', '0');
if ($lastReached < ($this->timeFactory->getTime() - 7 * 24 * 3600)) {
return SetupResult::success();
}
return SetupResult::error(
$this->l->t('Nextcloud GmbH sponsors a free push notification gateway for private users. To ensure good service, the gateway limits the number of push notifications per server and the limit was reached for this server. For enterprise users, a more scalable gateway is available.'),
'https://nextcloud.com/fairusepolicy'
);
}
}