Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow to test push notifications #620

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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