Skip to content

Commit

Permalink
Implement Notifications (#213)
Browse files Browse the repository at this point in the history
* Implement Notifications

* Notification adjustments

* Add documentation
* Remove code smells
* Update psalm-baseline to ignore IRootFolder dependency
  • Loading branch information
R0Wi authored Jul 30, 2023
1 parent 02f01b8 commit 3aebf0f
Show file tree
Hide file tree
Showing 15 changed files with 995 additions and 35 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
- [Per workflow settings](#per-workflow-settings)
- [Global settings](#global-settings)
- [Testing your configuration](#testing-your-configuration)
- [Get feedback via Notifications](#get-feedback-via-notifications)
- [How it works](#how-it-works)
- [General](#general)
- [PDF](#pdf)
Expand Down Expand Up @@ -165,6 +166,15 @@ To **test** if your file gets processed properly you can do the following steps:
<img width="75%" src="doc/img/file_versions.jpg" alt="File versions">
</p>

### Get feedback via Notifications

The Workflow OCR app supports sending notifications to the user in case anything went wrong during the [asynchronous OCR processing](#how-it-works). To enable this feature, you have to install and enable the [`Notifications`](https://github.com/nextcloud/notifications) app in your Nextcloud instance.

<p align="center">
<img width="30%" src="doc/img/notifications.png" alt="Notifications">
</p>


## How it works
### General
<p align="center">
Expand Down
Binary file added doc/img/notifications.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,17 @@
use OCA\WorkflowOcr\Helper\ProcessingFileAccessor;
use OCA\WorkflowOcr\Helper\SidecarFileAccessor;
use OCA\WorkflowOcr\Listener\RegisterFlowOperationsListener;
use OCA\WorkflowOcr\Notification\Notifier;
use OCA\WorkflowOcr\OcrProcessors\IOcrProcessorFactory;
use OCA\WorkflowOcr\OcrProcessors\OcrProcessorFactory;
use OCA\WorkflowOcr\Service\EventService;
use OCA\WorkflowOcr\Service\GlobalSettingsService;
use OCA\WorkflowOcr\Service\IEventService;
use OCA\WorkflowOcr\Service\IGlobalSettingsService;
use OCA\WorkflowOcr\Service\INotificationService;
use OCA\WorkflowOcr\Service\IOcrBackendInfoService;
use OCA\WorkflowOcr\Service\IOcrService;
use OCA\WorkflowOcr\Service\NotificationService;
use OCA\WorkflowOcr\Service\OcrBackendInfoService;
use OCA\WorkflowOcr\Service\OcrService;
use OCA\WorkflowOcr\Wrapper\CommandWrapper;
Expand Down Expand Up @@ -78,6 +81,7 @@ public function register(IRegistrationContext $context): void {
$context->registerServiceAlias(IGlobalSettingsService::class, GlobalSettingsService::class);
$context->registerServiceAlias(IEventService::class, EventService::class);
$context->registerServiceAlias(IOcrBackendInfoService::class, OcrBackendInfoService::class);
$context->registerServiceAlias(INotificationService::class, NotificationService::class);

// BUG #43
$context->registerService(ICommand::class, function () {
Expand All @@ -94,6 +98,7 @@ public function register(IRegistrationContext $context): void {
OcrProcessorFactory::registerOcrProcessors($context);

$context->registerEventListener(RegisterOperationsEvent::class, RegisterFlowOperationsListener::class);
$context->registerNotifierService(Notifier::class);
}

/**
Expand Down
54 changes: 37 additions & 17 deletions lib/BackgroundJobs/ProcessFileJob.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
use OCA\WorkflowOcr\Helper\IProcessingFileAccessor;
use OCA\WorkflowOcr\Model\WorkflowSettings;
use OCA\WorkflowOcr\Service\IEventService;
use OCA\WorkflowOcr\Service\INotificationService;
use OCA\WorkflowOcr\Service\IOcrService;
use OCA\WorkflowOcr\Wrapper\IFilesystem;
use OCA\WorkflowOcr\Wrapper\IViewFactory;
Expand Down Expand Up @@ -69,6 +70,8 @@ class ProcessFileJob extends \OCP\BackgroundJob\QueuedJob {
private $userSession;
/** @var IProcessingFileAccessor */
private $processingFileAccessor;
/** @var INotificationService */
private $notificationService;

public function __construct(
LoggerInterface $logger,
Expand All @@ -80,6 +83,7 @@ public function __construct(
IUserManager $userManager,
IUserSession $userSession,
IProcessingFileAccessor $processingFileAccessor,
INotificationService $notificationService,
ITimeFactory $timeFactory) {
parent::__construct($timeFactory);
$this->logger = $logger;
Expand All @@ -91,6 +95,7 @@ public function __construct(
$this->userManager = $userManager;
$this->userSession = $userSession;
$this->processingFileAccessor = $processingFileAccessor;
$this->notificationService = $notificationService;
}

/**
Expand All @@ -101,14 +106,16 @@ protected function run($argument) : void {

[$success, $filePath, $uid, $settings] = $this->tryParseArguments($argument);
if (!$success) {
$this->notificationService->createErrorNotification($uid, 'Failed to parse arguments inside the OCR process. Please have a look at your servers logfile for more details.');
return;
}

try {
$this->initUserEnvironment($uid);
$this->processFile($filePath, $settings);
$this->processFile($filePath, $settings, $uid);
} catch (\Throwable $ex) {
$this->logger->error($ex->getMessage(), ['exception' => $ex]);
$this->notificationService->createErrorNotification($uid, 'An error occured while executing the OCR process. Please have a look at your servers logfile for more details.');
} finally {
$this->shutdownUserEnvironment();
}
Expand Down Expand Up @@ -165,26 +172,35 @@ private function tryParseArguments($argument) : array {
/**
* @param string $filePath The file to be processed
* @param WorkflowSettings $settings The settings to be used for processing
* @param string $userId The user who triggered the processing
*/
private function processFile(string $filePath, WorkflowSettings $settings) : void {
$node = $this->getNode($filePath);
private function processFile(string $filePath, WorkflowSettings $settings, string $userId) : void {
$node = $this->getNode($filePath, $userId);

if ($node === null) {
return;
}

$nodeId = $node->getId();

try {
$ocrFile = $this->ocrService->ocrFile($node, $settings);
} catch (OcrNotPossibleException $ocrNpEx) {
$this->logger->error('OCR for file ' . $node->getPath() . ' not possible. Message: ' . $ocrNpEx->getMessage());
return;
} catch (OcrProcessorNotFoundException $ocrNfEx) {
$this->logger->error('OCR processor not found for mimetype ' . $node->getMimeType());
} catch(\Throwable $throwable) {
if ($throwable instanceof(OcrNotPossibleException::class)) {
$msg = 'OCR for file ' . $node->getPath() . ' not possible. Message: ' . $throwable->getMessage();
} elseif ($throwable instanceof(OcrProcessorNotFoundException::class)) {
$msg = 'OCR processor not found for mimetype ' . $node->getMimeType();
} else {
throw $throwable;
}

$this->logger->error($msg);
$this->notificationService->createErrorNotification($userId, $msg, $nodeId);

return;
}

$fileContent = $ocrFile->getFileContent();
$nodeId = $node->getId();
$originalFileExtension = $node->getExtension();
$newFileExtension = $ocrFile->getFileExtension();

Expand All @@ -200,35 +216,39 @@ private function processFile(string $filePath, WorkflowSettings $settings) : voi
$this->eventService->textRecognized($ocrFile, $node);
}

private function getNode(string $filePath) : ?Node {
private function getNode(string $filePath, string $userId) : ?Node {
try {
/** @var File */
$node = $this->rootFolder->get($filePath);
} catch (NotFoundException $nfEx) {
$this->logger->warning('Could not process file \'' . $filePath . '\'. File was not found');
$msg = 'Could not process file \'' . $filePath . '\'. File was not found';
$this->logger->warning($msg);
$this->notificationService->createErrorNotification($userId, $msg);
return null;
}

if (!$node instanceof Node || $node->getType() !== FileInfo::TYPE_FILE) {
$this->logger->warning('Skipping process for \'' . $filePath . '\'. It is not a file');
$msg = 'Skipping process for \'' . $filePath . '\'. It is not a file';
$this->logger->warning($msg);
$this->notificationService->createErrorNotification($userId, $msg);
return null;
}

return $node;
}

/**
* * @param string $uid The owners userId of the file to be processed
* * @param string $userId The owners userId of the file to be processed
*/
private function initUserEnvironment(string $uid) : void {
private function initUserEnvironment(string $userId) : void {
/** @var IUser */
$user = $this->userManager->get($uid);
$user = $this->userManager->get($userId);
if (!$user) {
throw new NoUserException("User with uid '$uid' was not found");
throw new NoUserException("User with uid '$userId' was not found");
}

$this->userSession->setUser($user);
$this->filesystem->init($uid, '/' . $uid . '/files');
$this->filesystem->init($userId, '/' . $userId . '/files');
}

private function shutdownUserEnvironment() : void {
Expand Down
127 changes: 127 additions & 0 deletions lib/Notification/Notifier.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
<?php

declare(strict_types=1);

/**
* @copyright Copyright (c) 2023 Robin Windey <[email protected]>
*
* @author Robin Windey <[email protected]>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

namespace OCA\WorkflowOcr\Notification;

use OCA\WorkflowOcr\AppInfo\Application;
use OCP\Files\File;
use OCP\Files\IRootFolder;
use OCP\IURLGenerator;
use OCP\L10N\IFactory;
use OCP\Notification\AlreadyProcessedException;
use OCP\Notification\INotification;
use OCP\Notification\INotifier;
use Psr\Log\LoggerInterface;

class Notifier implements INotifier {
/** @var IFactory*/
private $l10nFactory;
/** @var IURLGenerator */
private $urlGenerator;
/** @var IRootFolder */
private $rootFolder;
/** @var LoggerInterface */
private $logger;

public function __construct(IFactory $factory,
IURLGenerator $urlGenerator,
IRootFolder $rootFolder,
LoggerInterface $logger) {
$this->l10nFactory = $factory;
$this->urlGenerator = $urlGenerator;
$this->rootFolder = $rootFolder;
$this->logger = $logger;
}

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

/**
* Human readable name describing the notifier
* @return string
*/
public function getName(): string {
return $this->l10nFactory->get(Application::APP_NAME)->t('Workflow OCR');
}

/**
* @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() !== Application::APP_NAME) {
throw new \InvalidArgumentException();
}

$notification->setIcon($this->urlGenerator->imagePath(Application::APP_NAME, 'app-dark.svg'));
$l = $this->l10nFactory->get(Application::APP_NAME, $languageCode);

// Currently we only support sending notifications for ocr_error
if ($notification->getSubject() !== 'ocr_error') {
throw new \InvalidArgumentException();
}

$message = $notification->getSubjectParameters()['message'];
$notification
->setParsedSubject($l->t('Workflow OCR error'))
->setParsedMessage($message);
// Only add file info if we have one ...
if ($notification->getObjectType() === 'file' && $notification->getObjectId()) {
$richParams = $this->getRichParamForFile($notification);
$notification->setRichSubject($l->t('Workflow OCR error for file {file}'), $richParams);
}
return $notification;
}

private function getRichParamForFile(INotification $notification) : array {
try {
$userFolder = $this->rootFolder->getUserFolder($notification->getUser());
/** @var File[] */
$files = $userFolder->getById($notification->getObjectId());
/** @var File $file */
$file = array_shift($files);
$relativePath = $userFolder->getRelativePath($file->getPath());
} catch (\Throwable $th) {
$this->logger->error($th->getMessage(), ['exception' => $th]);
throw new AlreadyProcessedException();
}

return [
'file' => [
'type' => 'file',
'id' => $file->getId(),
'name' => $file->getName(),
'path' => $relativePath,
'link' => $this->urlGenerator->linkToRouteAbsolute('files.viewcontroller.showFile', ['fileid' => $notification->getObjectId()])
]
];
}
}
3 changes: 3 additions & 0 deletions lib/Service/EventService.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ public function __construct(IEventDispatcher $eventDispatcher) {
$this->eventDispatcher = $eventDispatcher;
}

/**
* @return void
*/
public function textRecognized(OcrProcessorResult $result, File $node) {
$event = new TextRecognizedEvent($result->getRecognizedText(), $node);
$this->eventDispatcher->dispatchTyped($event);
Expand Down
38 changes: 38 additions & 0 deletions lib/Service/INotificationService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

/**
* @copyright Copyright (c) 2023 Robin Windey <[email protected]>
*
* @author Robin Windey <[email protected]>
*
* @license GNU AGPL version 3 or any later version
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/

namespace OCA\WorkflowOcr\Service;

interface INotificationService {

/**
* Create a new notification for the given user if the OCR process of the given file failed.
* @param string $userId The user ID of the user that should receive the notification.
* @param string $message The error message that should be displayed in the notification.
* @param int $fileId Optional file ID of the file that failed to OCR. If given, user can jump to the file via link.
*/
public function createErrorNotification(?string $userId, string $message, int $fileId = null);
}
Loading

0 comments on commit 3aebf0f

Please sign in to comment.