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

Group delete pushes #1429

Merged
merged 7 commits into from
Jan 31, 2023
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
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