Skip to content

Commit

Permalink
Merge pull request #1973 from nextcloud/enh/add-rich-object-support-t…
Browse files Browse the repository at this point in the history
…o-api

Add rich object support to API
  • Loading branch information
nickvergessen authored Aug 12, 2024
2 parents 303685f + 78be17b commit 180fac9
Show file tree
Hide file tree
Showing 12 changed files with 1,000 additions and 273 deletions.
1 change: 1 addition & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
['name' => 'Endpoint#deleteAllNotifications', 'url' => '/api/{apiVersion}/notifications', 'verb' => 'DELETE', 'requirements' => ['apiVersion' => '(v1|v2)']],

['name' => 'API#generateNotification', 'url' => '/api/{apiVersion}/admin_notifications/{userId}', 'verb' => 'POST', 'requirements' => ['apiVersion' => '(v1|v2)']],
['name' => 'API#generateNotificationV3', 'url' => '/api/{apiVersion3}/admin_notifications/{userId}', 'verb' => 'POST', 'requirements' => ['apiVersion3' => '(v3)']],

['name' => 'Settings#personal', 'url' => '/api/{apiVersion}/settings', 'verb' => 'POST', 'requirements' => ['apiVersion' => '(v2)']],
['name' => 'Settings#admin', 'url' => '/api/{apiVersion}/settings/admin', 'verb' => 'POST', 'requirements' => ['apiVersion' => '(v2)']],
Expand Down
9 changes: 7 additions & 2 deletions lib/App.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use Symfony\Component\Console\Output\OutputInterface;

class App implements IDeferrableApp {
protected ?int $lastInsertedId = null;
public function __construct(
protected Handler $handler,
protected Push $push,
Expand All @@ -33,15 +34,19 @@ public function setOutput(OutputInterface $output): void {
* @since 8.2.0
*/
public function notify(INotification $notification): void {
$notificationId = $this->handler->add($notification);
$this->lastInsertedId = $this->handler->add($notification);

try {
$this->push->pushToDevice($notificationId, $notification);
$this->push->pushToDevice($this->lastInsertedId, $notification);
} catch (NotificationNotFoundException $e) {
$this->logger->error('Error while preparing push notification', ['exception' => $e]);
}
}

public function getLastInsertedId(): ?int {
return $this->lastInsertedId;
}

/**
* @param INotification $notification
* @return int
Expand Down
97 changes: 85 additions & 12 deletions lib/Controller/APIController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

namespace OCA\Notifications\Controller;

use OCA\Notifications\App;
use OCA\Notifications\ResponseDefinitions;
use OCP\AppFramework\Http;
use OCP\AppFramework\Http\Attribute\OpenAPI;
use OCP\AppFramework\Http\DataResponse;
Expand All @@ -18,65 +20,136 @@
use OCP\IUser;
use OCP\IUserManager;
use OCP\Notification\IManager;
use OCP\Notification\IncompleteNotificationException;
use OCP\Notification\InvalidValueException;
use OCP\RichObjectStrings\InvalidObjectExeption;
use OCP\RichObjectStrings\IValidator;
use Psr\Log\LoggerInterface;

/**
* @psalm-import-type NotificationsRichObjectParameter from ResponseDefinitions
*/
class APIController extends OCSController {
public function __construct(
string $appName,
IRequest $request,
protected ITimeFactory $timeFactory,
protected IUserManager $userManager,
protected IManager $notificationManager,
protected App $notificationApp,
protected IValidator $richValidator,
protected LoggerInterface $logger,
) {
parent::__construct($appName, $request);
}

/**
* Generate a notification for a user
* Generate a notification for a user (deprecated, use v3 instead)
*
* @param string $userId ID of the user
* @param string $shortMessage Subject of the notification
* @param string $longMessage Message of the notification
* @return DataResponse<Http::STATUS_OK, array<empty>, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, null, array{}>
* @deprecated 30.0.0
*
* 200: Notification generated successfully
* 400: Generating notification is not possible
* 404: User not found
*/
#[OpenAPI(scope: OpenAPI::SCOPE_ADMINISTRATION)]
public function generateNotification(string $userId, string $shortMessage, string $longMessage = ''): DataResponse {
$response = $this->generateNotificationV3($userId, $shortMessage, $longMessage);
if ($response->getStatus() === Http::STATUS_OK) {
return new DataResponse();
}

// Translate to old status code
$error = $response->getData()['error'] ?? null;
$code = match($error) {
'user' => Http::STATUS_NOT_FOUND,
'subject',
'message' => Http::STATUS_BAD_REQUEST,
default => Http::STATUS_INTERNAL_SERVER_ERROR,
};
return new DataResponse(null, $code);
}

/**
* Generate a notification with rich object parameters for a user
*
* @param string $userId ID of the user
* @param string $subject Subject of the notification
* @param string $message Message of the notification
* @param array<string, NotificationsRichObjectParameter> $subjectParameters Rich objects to fill the subject placeholders, {@see \OCP\RichObjectStrings\Definitions}
* @param array<string, NotificationsRichObjectParameter> $messageParameters Rich objects to fill the message placeholders, {@see \OCP\RichObjectStrings\Definitions}
* @return DataResponse<Http::STATUS_OK, array{id: int}, array{}>|DataResponse<Http::STATUS_BAD_REQUEST, array{error: string}, array{}>
*
* 200: Notification generated successfully, returned id is the notification ID for future delete requests
* 400: Provided data was invalid, check error field of the response of log file for details
*/
#[OpenAPI(scope: OpenAPI::SCOPE_ADMINISTRATION)]
public function generateNotificationV3(
string $userId,
string $subject = '',
string $message = '',
array $subjectParameters = [],
array $messageParameters = [],
): DataResponse {
$user = $this->userManager->get($userId);

if (!$user instanceof IUser) {
return new DataResponse(null, Http::STATUS_NOT_FOUND);
return new DataResponse(['error' => 'user'], Http::STATUS_BAD_REQUEST);
}

if ($shortMessage === '' || strlen($shortMessage) > 255) {
return new DataResponse(null, Http::STATUS_BAD_REQUEST);
if ($subject === '' || strlen($subject) > 255) {
return new DataResponse(['error' => 'subject'], Http::STATUS_BAD_REQUEST);
}

if ($longMessage !== '' && strlen($longMessage) > 4000) {
return new DataResponse(null, Http::STATUS_BAD_REQUEST);
if ($message !== '' && strlen($message) > 4000) {
return new DataResponse(['error' => 'message'], Http::STATUS_BAD_REQUEST);
}

$notification = $this->notificationManager->createNotification();
$datetime = $this->timeFactory->getDateTime();

try {
if (!empty($subjectParameters)) {
$this->richValidator->validate($subject, $subjectParameters);
}
if ($message !== '' && !empty($messageParameters)) {
$this->richValidator->validate($message, $messageParameters);
}
$notification->setApp('admin_notifications')
->setUser($user->getUID())
->setDateTime($datetime)
->setObject('admin_notifications', dechex($datetime->getTimestamp()))
->setSubject('ocs', [$shortMessage]);
->setSubject(
'ocs',
[
'subject' => $subject,
'parameters' => $subjectParameters,
]
);

if ($longMessage !== '') {
$notification->setMessage('ocs', [$longMessage]);
if ($message !== '') {
$notification->setMessage(
'ocs',
[
'message' => $message,
'parameters' => $messageParameters,
]
);
}

$this->notificationManager->notify($notification);
} catch (\InvalidArgumentException) {
return new DataResponse(null, Http::STATUS_INTERNAL_SERVER_ERROR);
} catch (InvalidObjectExeption $e) {
$this->logger->error('Invalid rich object parameter provided: ' . $e->getMessage(), ['exception' => $e]);
return new DataResponse(['error' => 'parameters'], Http::STATUS_BAD_REQUEST);
} catch (InvalidValueException|IncompleteNotificationException $e) {
$this->logger->error('Invalid value for notification provided: ' . $e->getMessage(), ['exception' => $e]);
return new DataResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
}

return new DataResponse();
return new DataResponse(['id' => (int) $this->notificationApp->getLastInsertedId()]);
}
}
18 changes: 15 additions & 3 deletions lib/Notifier/AdminNotifications.php
Original file line number Diff line number Diff line change
Expand Up @@ -176,10 +176,22 @@ public function prepare(INotification $notification, string $languageCode): INot
case 'cli':
case 'ocs':
$subjectParams = $notification->getSubjectParameters();
$notification->setParsedSubject($subjectParams[0]);
if (isset($subjectParams['subject'])) {
// Nextcloud 30+
$notification->setRichSubject($subjectParams['subject'], $subjectParams['parameters']);
} else {
// Legacy before Nextcloud 30 (v3)
$notification->setParsedSubject($subjectParams[0]);
}
$messageParams = $notification->getMessageParameters();
if (isset($messageParams[0]) && $messageParams[0] !== '') {
$notification->setParsedMessage($messageParams[0]);
if (!empty($messageParams)) {
if (!empty($messageParams['message'])) {
// Nextcloud 30+
$notification->setRichMessage($messageParams['message'], $messageParams['parameters']);
} elseif (!empty($messageParams[0])) {
// Legacy before Nextcloud 30 (v3)
$notification->setParsedMessage($messageParams[0]);
}
}

$notification->setIcon($this->urlGenerator->getAbsoluteURL($this->urlGenerator->imagePath('notifications', 'notifications-dark.svg')));
Expand Down
34 changes: 32 additions & 2 deletions lib/ResponseDefinitions.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,36 @@
namespace OCA\Notifications;

/**
* @psalm-type NotificationsRichObjectParameter = array{
* type: string,
* id: string,
* name: string,
* server?: string,
* link?: string,
* 'call-type'?: 'one2one'|'group'|'public',
* 'icon-url'?: string,
* 'message-id'?: string,
* boardname?: string,
* stackname?: string,
* size?: string,
* path?: string,
* mimetype?: string,
* 'preview-available'?: 'yes'|'no',
* mtime?: string,
* latitude?: string,
* longitude?: string,
* description?: string,
* thumb?: string,
* website?: string,
* visibility?: '0'|'1',
* assignable?: '0'|'1',
* conversation?: string,
* etag?: string,
* permissions?: string,
* width?: string,
* height?: string,
* }
*
* @psalm-type NotificationsNotificationAction = array{
* label: string,
* link: string,
Expand All @@ -29,9 +59,9 @@
* link: string,
* actions: NotificationsNotificationAction[],
* subjectRich?: string,
* subjectRichParameters?: array<string, mixed>,
* subjectRichParameters?: array<string, NotificationsRichObjectParameter>,
* messageRich?: string,
* messageRichParameters?: array<string, mixed>,
* messageRichParameters?: array<string, NotificationsRichObjectParameter>,
* icon?: string,
* shouldNotify?: bool,
* }
Expand Down
Loading

0 comments on commit 180fac9

Please sign in to comment.