Skip to content

Commit

Permalink
Merge pull request #1429 from nextcloud/redo/939/group-delete-pushes
Browse files Browse the repository at this point in the history
Group delete pushes
  • Loading branch information
nickvergessen authored Jan 31, 2023
2 parents 9990277 + b6fd592 commit 4067a44
Show file tree
Hide file tree
Showing 3 changed files with 120 additions and 27 deletions.
2 changes: 1 addition & 1 deletion lib/App.php
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public function markProcessed(INotification $notification): void {
}
foreach ($deleted as $user => $notifications) {
foreach ($notifications as $data) {
$this->push->pushDeleteToDevice((string) $user, $data['id'], $data['app']);
$this->push->pushDeleteToDevice((string) $user, [$data['id']], $data['app']);
}
}
if (!$isAlreadyDeferring) {
Expand Down
4 changes: 2 additions & 2 deletions lib/Controller/EndpointController.php
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ public function deleteNotification(int $id): DataResponse {
$deleted = $this->handler->deleteById($id, $this->getCurrentUser(), $notification);

if ($deleted) {
$this->push->pushDeleteToDevice($this->getCurrentUser(), $id, $notification->getApp());
$this->push->pushDeleteToDevice($this->getCurrentUser(), [$id], $notification->getApp());
}
} catch (NotificationNotFoundException $e) {
}
Expand All @@ -207,7 +207,7 @@ public function deleteAllNotifications(): DataResponse {

$deletedSomething = $this->handler->deleteByUser($this->getCurrentUser());
if ($deletedSomething) {
$this->push->pushDeleteToDevice($this->getCurrentUser(), 0);
$this->push->pushDeleteToDevice($this->getCurrentUser(), null);
}

if ($shouldFlush) {
Expand Down
141 changes: 117 additions & 24 deletions lib/Push.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,21 +70,38 @@ class Push {
protected $log;
/** @var OutputInterface */
protected $output;
/** @var array */
/**
* @var array
* @psalm-var array<string, list<string>>
*/
protected $payloadsToSend = [];

/** @var bool */
protected $deferPreparing = false;
/** @var bool */
protected $deferPayloads = false;
/** @var array[] */
/**
* @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 null[]|IUserStatus[] */
/**
* @var ?IUserStatus[]
* @psalm-var array<string, ?IUserStatus>
*/
protected $userStatuses = [];
/** @var array[] */
/**
* @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 = [];
Expand Down Expand Up @@ -170,9 +187,24 @@ public function flushPayloads(): void {
$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 $id => $data) {
$this->pushDeleteToDevice($data['userId'], $id, $data['app']);
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 = [];
}
Expand All @@ -181,6 +213,13 @@ public function flushPayloads(): void {
$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);

Expand Down Expand Up @@ -310,17 +349,47 @@ public function pushToDevice(int $id, INotification $notification, ?OutputInterf
}
}

public function pushDeleteToDevice(string $userId, int $notificationId, string $app = ''): void {
/**
* @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) {
$this->deletesToPush[$notificationId] = ['userId' => $userId, 'app' => $app];
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)) {
Expand All @@ -330,8 +399,8 @@ public function pushDeleteToDevice(string $userId, int $notificationId, string $
$devices = $this->userDevices[$userId];
}

if ($notificationId !== 0 && $app !== '') {
// Only filter when it's not a single delete
if (!$deleteAll) {
// Only filter when it's not delete-all
$devices = $this->filterDeviceList($devices, $app);
}
if (empty($devices)) {
Expand All @@ -350,13 +419,23 @@ public function pushDeleteToDevice(string $userId, int $notificationId, string $
}

try {
$payload = json_encode($this->encryptAndSignDelete($userKey, $device, $notificationId));

$proxyServer = rtrim($device['proxyserver'], '/');
if (!isset($this->payloadsToSend[$proxyServer])) {
$this->payloadsToSend[$proxyServer] = [];
}
$this->payloadsToSend[$proxyServer][] = $payload;

if ($deleteAll) {
$data = $this->encryptAndSignDelete($userKey, $device, null);
$this->payloadsToSend[$proxyServer][] = json_encode($data['payload']);
} else {
$temp = $notificationIds;

while (!empty($temp)) {
$data = $this->encryptAndSignDelete($userKey, $device, $temp);
$temp = $data['remaining'];
$this->payloadsToSend[$proxyServer][] = json_encode($data['payload']);
}
}
} catch (\InvalidArgumentException $e) {
// Failed to encrypt message for device: public key is invalid
$this->deletePushToken($device['token']);
Expand Down Expand Up @@ -500,6 +579,7 @@ protected function validateToken(int $tokenId, int $maxAge): bool {
* @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
*/
Expand Down Expand Up @@ -562,21 +642,29 @@ protected function encryptAndSign(Key $userKey, array $device, int $id, INotific
/**
* @param Key $userKey
* @param array $device
* @param int $id
* @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, int $id): array {
if ($id === 0) {
protected function encryptAndSignDelete(Key $userKey, array $device, ?array $ids): array {
$remainingIds = [];
if ($ids === null) {
$data = [
'delete-all' => true,
];
} else {
} elseif (count($ids) === 1) {
$data = [
'nid' => $id,
'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)) {
Expand All @@ -589,18 +677,22 @@ protected function encryptAndSignDelete(Key $userKey, array $device, int $id): a
$base64Signature = base64_encode($signature);

return [
'deviceIdentifier' => $device['deviceidentifier'],
'pushTokenHash' => $device['pushtokenhash'],
'subject' => $base64EncryptedSubject,
'signature' => $base64Signature,
'priority' => 'normal',
'type' => 'background',
'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();
Expand All @@ -618,6 +710,7 @@ protected function getDevicesForUser(string $uid): array {
/**
* @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();
Expand Down

0 comments on commit 4067a44

Please sign in to comment.