<!--
  - SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
  - SPDX-FileCopyrightText: 2015-2016 ownCloud, Inc.
  - SPDX-License-Identifier: AGPL-3.0-only
-->
# Notification Workflow for an App that sends Notifications

## Example story

Let's assume the following example scenario. Our app is the files_sharing app. We want
to notify the user when a remote share has to be accepted/declined. If the user has dealt
with it, we want to remove the notification again.

### Creating a new Notification

  1. Grab a new notification object (`\OCP\Notification\INotification`) from the manager
  (`\OCP\Notification\IManager`):
```php
$manager = \OCP\Server::get(\OCP\Notification\IManager::class);
$notification = $manager->createNotification();
```

  2. Set the necessary information for the notification:
```php
$acceptAction = $notification->createAction();
$acceptAction->setLabel('accept')
    ->setLink('remote_shares', 'POST');

$declineAction = $notification->createAction();
$declineAction->setLabel('decline')
    ->setLink('remote_shares', 'DELETE');

$notification->setApp('files_sharing')
    ->setUser('recipient1')
    ->setDateTime(new \DateTime())
    ->setObject('remote', '1337') // $type and $id
    ->setSubject('remote_share', ['name' => '/fancyFolder']) // $subject and $parameters
    ->addAction($acceptAction)
    ->addAction($declineAction)
;
```
  Setting **app, user, timestamp, object and subject** are mandatory. You should not use a
  translated subject, message or action label. Use something like a "language key", to avoid
  length problems with translations in the storage of a notification app. Translation is done
  via invocation of your notifier by the manager when the notification is prepared for display.

  You should also try to avoid setting links and image paths here already, use keys again instead.
  This allows you to change the image/url of your application and also the admin can move the instance
  to another domain later, without breaking pending notifications. Also make sure, all your URLs are
  absolute URLs, so the notification icon and link also work from the desktop and mobile clients.

  3. Send the notification back to the manager:
```php
$manager->notify($notification);
```

### Preparing a notification for display

1. In `OCA\MyApp\Application::register(IRegistrationContext $context)` register your Notifier (implementing the `\OCP\Notification\INotifier` interface) to the manager,
  using a `\Closure` returning the Notifier and a `\Closure` returning an array of the id and name:
```php
	public function register(IRegistrationContext $context): void {
		$context->registerNotifierService(\OCA\Files_Sharing\Notification\Notifier::class);
	}
```

2. The manager will execute the closure and then call the `prepare()` method on your notifier.
  If the notification is not known by your app, just throw an `OCP\Notification\UnknownNotificationException` (*Added in Nextcloud 30, before throw `\InvalidArgumentException`*),
  but if it is actually from your app, you must set the parsed subject, message and action labels.
  If the notification is obsolete by now, e.g. because the share was revoked already, you can throw a `OCP\Notification\AlreadyProcessedException` which will remove the notification also from the devices.
```php

class Notifier implements \OCP\Notification\INotifier {
	protected $factory;
	protected $url;

	public function __construct(\OCP\L10N\IFactory $factory,
								\OCP\IURLGenerator $urlGenerator) {
		$this->factory = $factory;
		$this->url = $urlGenerator;
	}

	/**
	 * Identifier of the notifier, only use [a-z0-9_]
	 * @return string
	 */
	public function getID(): string {
		return 'files_sharing';
	}

	/**
	 * Human-readable name describing the notifier
	 * @return string
	 */
	public function getName(): string {
		return $this->factory->get('files_sharing')->t('File sharing');
	}

	/**
	 * @param INotification $notification
	 * @param string $languageCode The code of the language that should be used to prepare the notification
	 */
	public function prepare(INotification $notification, string $languageCode): INotification {
		if ($notification->getApp() !== 'files_sharing') {
			// Not my app => throw
			throw new \OCP\Notification\UnknownNotificationException();
		}

		// Read the language from the notification
		$l = $this->factory->get('files_sharing', $languageCode);

		switch ($notification->getSubject()) {
			// Deal with known subjects
			case 'remote_share':
				try {
					$this->shareManager->getShareById($notification->getObjectId(), $notification->getUser());
				} catch (ShareNotFound $e) {
					// Throw AlreadyProcessedException exception when the notification has already been solved and can be removed.
					throw new \OCP\Notification\AlreadyProcessedException();
				}

				$notification->setIcon($this->url->getAbsoluteURL($this->url->imagePath('core', 'actions/share.svg')))
					->setLink($this->url->linkToRouteAbsolute('files_sharing.RemoteShare.overview', ['id' => $notification->getObjectId()]));

				// Set rich subject, see https://github.com/nextcloud/server/issues/1706 for more information
				// and https://github.com/nextcloud/server/blob/master/lib/public/RichObjectStrings/Definitions.php
				// for a list of defined objects and their parameters.
				$parameters = $notification->getSubjectParameters();
				$notification->setRichSubject($l->t('You received the remote share "{share}"'), [
					'share' => [
						'type' => 'pending-federated-share',
						'id' => $notification->getObjectId(),
						'name' => $parameters['name'],
					]
				]);

				// Deal with the actions for a known subject
				foreach ($notification->getActions() as $action) {
					switch ($action->getLabel()) {
						case 'accept':
							$action->setParsedLabel($l->t('Accept'))
								->setLink($this->url->linkToRouteAbsolute('files_sharing.RemoteShare.accept', ['id' => $notification->getObjectId()]), 'POST');
							break;

						case 'decline':
							$action->setParsedLabel($l->t('Decline'))
								->setLink($this->url->linkToRouteAbsolute('files_sharing.RemoteShare.decline', ['id' => $notification->getObjectId()]), 'DELETE');
							break;
					}

					$notification->addParsedAction($action);
				}

				// Set the plain text subject automatically
				$this->setParsedSubjectFromRichSubject($notification);
				return $notification;

			default:
				// Unknown subject => Unknown notification => throw
				throw new \OCP\Notification\UnknownNotificationException();
		}
	}

	/**
	 * This is a little helper function which automatically sets the simple parsed subject
	 * based on the rich subject you set. This is also the default behaviour of the API
	 * since Nextcloud 26, but in case you would like to return simpler or other strings,
	 * this function allows you to take over.
	 *
	 * @param INotification $notification
	 */
	protected function setParsedSubjectFromRichSubject(INotification $notification): void {
		$placeholders = $replacements = [];
		foreach ($notification->getRichSubjectParameters() as $placeholder => $parameter) {
			$placeholders[] = '{' . $placeholder . '}';
			if ($parameter['type'] === 'file') {
				$replacements[] = $parameter['path'];
			} else {
				$replacements[] = $parameter['name'];
			}
		}

		$notification->setParsedSubject(str_replace($placeholders, $replacements, $notification->getRichSubject()));
	}
}
```

### Marking a notification as read/deleted/processed/obsoleted

If the user accepted the share or the share was removed/unshared, we want to remove
the notification, because no user action is needed anymore. To do this, we simply have to
call the `markProcessed()` method on the manager with the necessary information on a
notification object:

```php
$manager = \OCP\Server::get(\OCP\Notification\IManager::class);
$notification->setApp('files_sharing')
    ->setObject('remote', 1337)
    ->setUser('recipient1');
$manager->markProcessed($notification);
```

Only the app name is mandatory: so if you don't set the user, the notification
will be marked as processed for all users that have it. So the following example will
remove all notifications for the app files_sharing on the object "remote #1337":

```php
$manager = \OCP\Server::get(\OCP\Notification\IManager::class);
$notification->setApp('files_sharing')
    ->setObject('remote', 1337);
$manager->markProcessed($notification);
```

### Defer and flush

Sometimes you might send multiple notifications in one request.
In that case it makes sense to defer the sending, so in the end only one connection
is done to the push server instead of 1 per notification.
```php
$manager = \OCP\Server::get(\OCP\Notification\IManager::class);
$shouldFlush = $manager->defer();

// Your application code generating notifications …

if ($shouldFlush) {
	// Only flush when defer() returned true, otherwise another app is already deferring
	$manager->flush();
}
```
