Send a push message when a notification was deleted

Signed-off-by: Joas Schilling <coding@schilljs.com>
This commit is contained in:
Joas Schilling 2019-04-10 16:01:43 +02:00
Родитель 8c378b4e6a
Коммит 7efe105bdb
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 7076EA9751AACDDA
8 изменённых файлов: 234 добавлений и 33 удалений

Просмотреть файл

@ -18,7 +18,8 @@ In order to find out if notifications support push on the server you can run a r
"push": [
...
"devices",
"object-data"
"object-data",
"delete"
]
}
}
@ -211,6 +212,8 @@ The pushed notifications is defined by the [Firebase Cloud Messaging HTTP Protoc
### Encrypted subject data
#### Normal content notification
If you are missing any information necessary to parse the notification in a more usable way, use the `nid` to get the full notification information via [OCS API](ocs-endpoint-v2.md)
```json
@ -220,7 +223,6 @@ If you are missing any information necessary to parse the notification in a more
"type" : "chat",
"id" : "t0k3n",
"nid" : 1337
}
}
```
@ -233,6 +235,38 @@ If you are missing any information necessary to parse the notification in a more
| `nid` | Numeric identifier of the notification in order to get more information via the [OCS API](ocs-endpoint-v2.md) | `object-data` |
#### Silent delete notification (single)
These notifications should not be shown to the user. Instead you should delete pending system notifications for the respective id
```json
{
"delete" : true,
"nid" : 1337
}
```
| Attribute | Meaning | Capability |
| ----------- | ---------------------------------------- |------------|
| `nid` | Numeric identifier of the notification in order to get more information via the [OCS API](ocs-endpoint-v2.md) | `object-data` |
| `delete` | Delete all notifications related to `nid` | `delete` |
#### Silent delete notification (all)
These notifications should not be shown to the user. Instead you should delete all pending system notifications for this account
```json
{
"delete-all" : true
}
```
| Attribute | Meaning | Capability |
| ----------- | ---------------------------------------- |------------|
| `delete-all` | Delete all notifications related to this account | `delete` |
### Verification
So a device should verify the signature using the user´s public key.
If the signature is okay, the subject can be decrypted using the device´s private key.

Просмотреть файл

@ -42,7 +42,7 @@ class App implements IApp {
* @throws \InvalidArgumentException When the notification is not valid
* @since 8.2.0
*/
public function notify(INotification $notification) {
public function notify(INotification $notification): void {
$notificationId = $this->handler->add($notification);
try {
@ -66,7 +66,13 @@ class App implements IApp {
* @param INotification $notification
* @since 8.2.0
*/
public function markProcessed(INotification $notification) {
$this->handler->delete($notification);
public function markProcessed(INotification $notification): void {
$deleted = $this->handler->delete($notification);
foreach ($deleted as $user => $notifications) {
foreach ($notifications as $notificationId) {
$this->push->pushDeleteToDevice($user, $notificationId);
}
}
}
}

Просмотреть файл

@ -41,31 +41,26 @@ class Application extends \OCP\AppFramework\App {
});
}
public function register() {
public function register(): void {
$this->registerNotificationApp();
$this->registerAdminNotifications();
$this->registerUserInterface();
}
protected function registerNotificationApp() {
$container = $this->getContainer();
$container->getServer()->getNotificationManager()->registerApp(function() use($container) {
return $container->query(App::class);
});
protected function registerNotificationApp(): void {
$this->getContainer()
->getServer()
->getNotificationManager()
->registerApp(App::class);
}
protected function registerAdminNotifications() {
$this->getContainer()->getServer()->getNotificationManager()->registerNotifier(function() {
return $this->getContainer()->query(AdminNotifications::class);
}, function() {
$l = $this->getContainer()->getServer()->getL10NFactory()->get('notifications');
return [
'id' => 'admin_notifications',
'name' => $l->t('Admin notifications'),
];
});
protected function registerAdminNotifications(): void {
$this->getContainer()
->getServer()
->getNotificationManager()
->registerNotifier(AdminNotifications::class);
}
protected function registerUserInterface() {
protected function registerUserInterface(): void {
// Only display the app on index.php except for public shares
$server = $this->getContainer()->getServer();
$request = $server->getRequest();

Просмотреть файл

@ -49,6 +49,7 @@ class Capabilities implements ICapability {
'push' => [
'devices',
'object-data',
'delete',
],
'admin-notifications' => [
'ocs',

Просмотреть файл

@ -23,6 +23,7 @@ namespace OCA\Notifications\Controller;
use OCA\Notifications\Exceptions\NotificationNotFoundException;
use OCA\Notifications\Handler;
use OCA\Notifications\Push;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\DataResponse;
use OCP\AppFramework\OCSController;
@ -37,15 +38,15 @@ use OCP\Notification\INotification;
class EndpointController extends OCSController {
/** @var Handler */
private $handler;
/** @var IManager */
private $manager;
/** @var IUserSession */
private $session;
/** @var IConfig */
private $config;
/** @var IUserSession */
private $session;
/** @var Push */
private $push;
/**
* @param string $appName
@ -54,14 +55,22 @@ class EndpointController extends OCSController {
* @param IManager $manager
* @param IConfig $config
* @param IUserSession $session
* @param Push $push
*/
public function __construct($appName, IRequest $request, Handler $handler, IManager $manager, IConfig $config, IUserSession $session) {
public function __construct(string $appName,
IRequest $request,
Handler $handler,
IManager $manager,
IConfig $config,
IUserSession $session,
Push $push) {
parent::__construct($appName, $request);
$this->handler = $handler;
$this->manager = $manager;
$this->config = $config;
$this->session = $session;
$this->push = $push;
}
/**
@ -154,6 +163,7 @@ class EndpointController extends OCSController {
}
$this->handler->deleteById($id, $this->getCurrentUser());
$this->push->pushDeleteToDevice($this->getCurrentUser(), $id);
return new DataResponse();
}
@ -164,6 +174,7 @@ class EndpointController extends OCSController {
*/
public function deleteAllNotifications(): DataResponse {
$this->handler->deleteByUser($this->getCurrentUser());
$this->push->pushDeleteToDevice($this->getCurrentUser(), 0);
return new DataResponse();
}

Просмотреть файл

@ -84,12 +84,33 @@ class Handler {
* 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) {
public function delete(INotification $notification): array {
$sql = $this->connection->getQueryBuilder();
$sql->delete('notifications');
$sql->select('notification_id', 'user')
->from('notifications');
$this->sqlWhere($sql, $notification);
$sql->execute();
$statement = $sql->execute();
$deleted = [];
while ($row = $statement->fetch()) {
if (!isset($deleted[$row['user']])) {
$deleted[$row['user']] = [];
}
$deleted[$row['user']][] = (int) $row['notification_id'];
}
$statement->closeCursor();
foreach ($deleted as $user => $notifications) {
foreach ($notifications as $notificationId) {
$this->deleteById($notificationId, $user);
}
}
return $deleted;
}
/**

Просмотреть файл

@ -25,6 +25,7 @@ namespace OCA\Notifications\Notifier;
use OCP\IURLGenerator;
use OCP\L10N\IFactory;
use OCP\Notification\AlreadyProcessedException;
use OCP\Notification\INotification;
use OCP\Notification\INotifier;
@ -45,13 +46,34 @@ class AdminNotifications implements INotifier {
$this->urlGenerator = $urlGenerator;
}
/**
* 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, $languageCode): INotification {
public function prepare(INotification $notification, string $languageCode): INotification {
if ($notification->getApp() !== 'admin_notifications') {
throw new \InvalidArgumentException('Unknown app');
}

Просмотреть файл

@ -66,7 +66,7 @@ class Push {
$this->log = $log;
}
public function pushToDevice(int $id, INotification $notification) {
public function pushToDevice(int $id, INotification $notification): void {
$user = $this->userManager->get($notification->getUser());
if (!($user instanceof IUser)) {
return;
@ -170,6 +170,76 @@ class Push {
}
}
public function pushDeleteToDevice(string $userId, int $notificationId): void {
$user = $this->userManager->get($userId);
if (!($user instanceof IUser)) {
return;
}
$devices = $this->getDevicesForUser($userId);
if (empty($devices)) {
return;
}
$userKey = $this->keyManager->getKey($user);
$pushNotifications = [];
foreach ($devices as $device) {
try {
$payload = json_encode($this->encryptAndSignDelete($userKey, $device, $notificationId));
$proxyServer = rtrim($device['proxyserver'], '/');
if (!isset($pushNotifications[$proxyServer])) {
$pushNotifications[$proxyServer] = [];
}
$pushNotifications[$proxyServer][] = $payload;
} catch (InvalidTokenException $e) {
// Token does not exist anymore, should drop the push device entry
$this->deletePushToken($device['token']);
} catch (\InvalidArgumentException $e) {
// Failed to encrypt message for device: public key is invalid
$this->deletePushToken($device['token']);
}
}
if (empty($pushNotifications)) {
return;
}
$client = $this->clientService->newClient();
foreach ($pushNotifications as $proxyServer => $notifications) {
try {
$response = $client->post($proxyServer . '/notifications', [
'body' => [
'notifications' => $notifications,
],
]);
} catch (\Exception $e) {
$this->log->logException($e, [
'app' => 'notifications',
'level' => $e->getCode() === Http::STATUS_BAD_REQUEST ? ILogger::INFO : ILogger::WARN,
]);
continue;
}
$status = $response->getStatusCode();
if ($status !== Http::STATUS_OK && $status !== Http::STATUS_SERVICE_UNAVAILABLE) {
$body = $response->getBody();
$this->log->error('Could not send notification to push server [{url}]: {error}',[
'error' => \is_string($body) ? $body : 'no reason given',
'url' => $proxyServer,
'app' => 'notifications',
]);
} else if ($status === Http::STATUS_SERVICE_UNAVAILABLE && $this->config->getSystemValue('debug', false)) {
$body = $response->getBody();
$this->log->debug('Could not send notification to push server [{url}]: {error}',[
'error' => \is_string($body) ? $body : 'no reason given',
'url' => $proxyServer,
'app' => 'notifications',
]);
}
}
}
/**
* @param Key $userKey
* @param array $device
@ -223,6 +293,47 @@ class Push {
];
}
/**
* @param Key $userKey
* @param array $device
* @param int $id
* @return array
* @throws InvalidTokenException
* @throws \InvalidArgumentException
*/
protected function encryptAndSignDelete(Key $userKey, array $device, int $id): array {
// Check if the token is still valid...
$this->tokenProvider->getTokenById($device['token']);
if ($id === 0) {
$data = [
'delete-all' => true,
];
} else {
$data = [
'nid' => $id,
'delete' => 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 [
'deviceIdentifier' => $device['deviceidentifier'],
'pushTokenHash' => $device['pushtokenhash'],
'subject' => $base64EncryptedSubject,
'signature' => $base64Signature,
'priority' => 'normal',
];
}
/**
* @param string $uid
* @return array[]