Skip to content

Commit

Permalink
Merge pull request #620 from nextcloud/feature/noid/allow-to-test-not…
Browse files Browse the repository at this point in the history
…ifications-better

Allow to test push notifications
  • Loading branch information
nickvergessen authored Apr 23, 2020
2 parents ab4a51b + 5355534 commit 87ed718
Show file tree
Hide file tree
Showing 6 changed files with 173 additions and 11 deletions.
1 change: 1 addition & 0 deletions appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,6 @@

<commands>
<command>OCA\Notifications\Command\Generate</command>
<command>OCA\Notifications\Command\TestPush</command>
</commands>
</info>
5 changes: 5 additions & 0 deletions lib/App.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
use OCA\Notifications\Exceptions\NotificationNotFoundException;
use OCP\Notification\IApp;
use OCP\Notification\INotification;
use Symfony\Component\Console\Output\OutputInterface;

class App implements IApp {
/** @var Handler */
Expand All @@ -37,6 +38,10 @@ public function __construct(Handler $handler, Push $push) {
$this->push = $push;
}

public function setOutput(OutputInterface $output): void {
$this->push->setOutput($output);
}

/**
* @param INotification $notification
* @throws \InvalidArgumentException When the notification is not valid
Expand Down
6 changes: 2 additions & 4 deletions lib/Command/Generate.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,15 +109,13 @@ protected function execute(InputInterface $input, OutputInterface $output): int
}

$notification = $this->notificationManager->createNotification();
$time = $this->timeFactory->getTime();
$datetime = new \DateTime();
$datetime->setTimestamp($time);
$datetime = $this->timeFactory->getDateTime();

try {
$notification->setApp('admin_notifications')
->setUser($user->getUID())
->setDateTime($datetime)
->setObject('admin_notifications', dechex($time))
->setObject('admin_notifications', dechex($datetime->getTimestamp()))
->setSubject('cli', [$subject]);

if ($message !== '') {
Expand Down
115 changes: 115 additions & 0 deletions lib/Command/TestPush.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
<?php
/**
* @copyright Copyright (c) 2017 Joas Schilling <[email protected]>
*
* @author Joas Schilling <[email protected]>
*
* @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\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() {
$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 {

$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;
}
}
2 changes: 1 addition & 1 deletion lib/Notifier/AdminNotifications.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public function getName(): string {
* @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') {
if ($notification->getApp() !== 'admin_notifications' && $notification->getApp() !== 'admin_notification_talk') {
throw new \InvalidArgumentException('Unknown app');
}

Expand Down
55 changes: 49 additions & 6 deletions lib/Push.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
use OCP\IUserManager;
use OCP\Notification\IManager as INotificationManager;
use OCP\Notification\INotification;
use Symfony\Component\Console\Output\OutputInterface;

class Push {
/** @var IDBConnection */
Expand All @@ -54,6 +55,8 @@ class Push {
protected $clientService;
/** @var ILogger */
protected $log;
/** @var OutputInterface */
protected $output;

public function __construct(IDBConnection $connection, INotificationManager $notificationManager, IConfig $config, IProvider $tokenProvider, Manager $keyManager, IUserManager $userManager, IClientService $clientService, ILogger $log) {
$this->db = $connection;
Expand All @@ -66,20 +69,37 @@ public function __construct(IDBConnection $connection, INotificationManager $not
$this->log = $log;
}

public function pushToDevice(int $id, INotification $notification): void {
public function setOutput(OutputInterface $output): void {
$this->output = $output;
}

protected function printInfo(string $message): void {
if ($this->output) {
$this->output->writeln($message);
}
}

public function pushToDevice(int $id, INotification $notification, ?OutputInterface $output = null): void {
$user = $this->userManager->get($notification->getUser());
if (!($user instanceof IUser)) {
return;
}

$devices = $this->getDevicesForUser($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->config->getSystemValue('force_language', false);
$language = \is_string($language) ? $language : $this->config->getUserValue($notification->getUser(), 'core', 'lang', null);
$language = $language ?? $this->config->getSystemValue('default_language', 'en');

$this->printInfo('Language is set to ' . $language);

try {
$this->notificationManager->setPreparingPushNotification(true);
$notification = $this->notificationManager->prepare($notification, $language);
Expand All @@ -91,26 +111,34 @@ public function pushToDevice(int $id, INotification $notification): void {

$userKey = $this->keyManager->getKey($user);

$isTalkNotification = \in_array($notification->getApp(), ['spreed', 'talk'], true);
$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);
$talkApps = array_filter($devices, function ($device) {
return $device['apptype'] === 'talk';
});
$hasTalkApps = !empty($talkApps);

$pushNotifications = [];
foreach ($devices as $device) {
$this->printInfo('');
$this->printInfo('Device token:' . $device['token']);

if (!$isTalkNotification && $device['apptype'] === 'talk') {
// The iOS app can not kill notifications,
// therefor we should only send relevant notifications to the Talk
// app, so it does not pollute the notifications bar with useless
// notifications, especially when the Sync client app is also installed.
$this->printInfo('Skipping talk device for non-talk notification');
continue;
}
if ($isTalkNotification && $hasTalkApps && $device['apptype'] !== 'talk') {
// Similar to the previous case, we also don't send Talk notifications
// to the Sync client app, when there is a Talk app installed. We only
// do this, when you don't have a Talk app on your device, so you still
// get the push notification.
$this->printInfo('Skipping other device for talk notification because a talk app is available');
continue;
}

Expand All @@ -124,6 +152,7 @@ public function pushToDevice(int $id, INotification $notification): void {
$pushNotifications[$proxyServer][] = $payload;
} catch (InvalidTokenException $e) {
// Token does not exist anymore, should drop the push device entry
$this->printInfo('InvalidTokenException is thrown');
$this->deletePushToken($device['token']);
} catch (\InvalidArgumentException $e) {
// Failed to encrypt message for device: public key is invalid
Expand Down Expand Up @@ -203,15 +232,20 @@ protected function sendNotificationsToProxies(array $pushNotifications): void {
$body = $response->getBody();
$bodyData = json_decode($body, true);
if ($status !== Http::STATUS_OK) {
$this->log->error('Could not send notification to push server [{url}]: {error}',[
'error' => \is_string($body) && $bodyData === null ? $body : 'no reason given',
$error = \is_string($body) && $bodyData === null ? $body : 'no reason given';
$this->printInfo('Could not send notification to push server [' . $proxyServer . ']: ' . $error);
$this->log->error('Could not send notification to push server [{url}]: {error}', [
'error' => $error,
'url' => $proxyServer,
'app' => 'notifications',
]);
} else {
$this->printInfo('Push notification sent');
}

if (is_array($bodyData) && !empty($bodyData['unknown']) && is_array($bodyData['unknown'])) {
foreach ($bodyData['unknown'] as $unknownDevice) {
$this->printInfo('Deleting device because it is unknown by the push server: ' . $unknownDevice);
$this->deletePushTokenByDeviceIdentifier($unknownDevice);
}
}
Expand Down Expand Up @@ -256,12 +290,21 @@ protected function encryptAndSign(Key $userKey, array $device, int $id, INotific
$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)) {
$this->log->error(openssl_error_string(), ['app' => 'notifications']);
$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');
}

openssl_sign($encryptedSubject, $signature, $userKey->getPrivate(), OPENSSL_ALGO_SHA512);
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);

Expand Down

0 comments on commit 87ed718

Please sign in to comment.