Send a push message when a notification was deleted
Signed-off-by: Joas Schilling <coding@schilljs.com>
This commit is contained in:
Родитель
8c378b4e6a
Коммит
7efe105bdb
|
@ -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.
|
||||
|
|
12
lib/App.php
12
lib/App.php
|
@ -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');
|
||||
}
|
||||
|
|
113
lib/Push.php
113
lib/Push.php
|
@ -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[]
|
||||
|
|
Загрузка…
Ссылка в новой задаче