From 8e81d703a2fc80165f84f7f8e2d477fe0e2495b9 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Thu, 21 Nov 2019 23:57:23 +0100 Subject: [PATCH 01/12] add flow operation, backend part Signed-off-by: Arthur Schiwon --- lib/AppInfo/Application.php | 2 + lib/Flow/Operation.php | 218 ++++++++++++++++++++++++++++++++++++ 2 files changed, 220 insertions(+) create mode 100644 lib/Flow/Operation.php diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index af2694eb628..07bebdb5926 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -37,6 +37,7 @@ use OCA\Talk\Events\RoomEvent; use OCA\Talk\Files\Listener as FilesListener; use OCA\Talk\Files\TemplateLoader as FilesTemplateLoader; +use OCA\Talk\Flow\Operation; use OCA\Talk\Listener; use OCA\Talk\Listener\LoadSidebarListener; use OCA\Talk\Listener\RestrictStartingCalls as RestrictStartingCallsListener; @@ -102,6 +103,7 @@ public function register(): void { CommandListener::register($dispatcher); ResourceListener::register($dispatcher); ChangelogListener::register($dispatcher); + Operation::register($dispatcher); $dispatcher->addServiceListener(AddContentSecurityPolicyEvent::class, Listener\CSPListener::class); $dispatcher->addServiceListener(AddFeaturePolicyEvent::class, Listener\FeaturePolicyListener::class); diff --git a/lib/Flow/Operation.php b/lib/Flow/Operation.php new file mode 100644 index 00000000000..5d0d5a6e49a --- /dev/null +++ b/lib/Flow/Operation.php @@ -0,0 +1,218 @@ + + * + * @author Arthur Schiwon + * + * @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 . + * + */ + +namespace OCA\Talk\Flow; + +use OCA\Talk\Chat\ChatManager; +use OCA\Talk\Exceptions\ParticipantNotFoundException; +use OCA\Talk\Exceptions\RoomNotFoundException; +use OCA\Talk\Manager as TalkManager; +use OCA\Talk\Participant; +use OCA\Talk\Room; +use OCP\EventDispatcher\Event; +use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\IUser; +use OCP\IUserSession; +use OCP\WorkflowEngine\IManager as FlowManager; +use OCP\WorkflowEngine\IOperation; +use OCP\WorkflowEngine\IRuleMatcher; +use Symfony\Component\EventDispatcher\GenericEvent; +use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; +use UnexpectedValueException; + +class Operation implements IOperation { + + /** @var IL10N */ + private $l; + /** @var IURLGenerator */ + private $urlGenerator; + /** @var TalkManager */ + private $talkManager; + /** @var IUserSession */ + private $session; + /** @var ChatManager */ + private $chatManager; + + public function __construct( + IL10N $l, + IURLGenerator $urlGenerator, + TalkManager $talkManager, + IUserSession $session, + ChatManager $chatManager + ) { + $this->l = $l; + $this->urlGenerator = $urlGenerator; + $this->talkManager = $talkManager; + $this->session = $session; + $this->chatManager = $chatManager; + } + + public static function register(EventDispatcherInterface $dispatcher): void { + $dispatcher->addListener(FlowManager::EVENT_NAME_REG_OPERATION, function (GenericEvent $event) { + $operation = \OC::$server->query(Operation::class); + $event->getSubject()->registerOperation($operation); + }); + } + + public function getDisplayName(): string { + return $this->l->t('Write to conversation'); + } + + public function getDescription(): string { + return $this->l->t('Writes event information into a conversation of your choice'); + } + + public function getIcon(): string { + return $this->urlGenerator->imagePath('spreed', 'app.svg'); + } + + public function isAvailableForScope(int $scope): bool { + return $scope === FlowManager::SCOPE_USER; + } + + /** + * Validates whether a configured workflow rule is valid. If it is not, + * an `\UnexpectedValueException` is supposed to be thrown. + * + * @throws UnexpectedValueException + * @since 9.1 + */ + public function validateOperation(string $name, array $checks, string $operation): void { + list($mode, $token) = $this->parseOperationConfig($operation); + $this->validateOperationConfig($mode, $token); + } + + /** + * Is being called by the workflow engine when an event was triggered that + * is configured for this operation. An evaluation whether the event + * qualifies for this operation to run has still to be done by the + * implementor by calling the RuleMatchers getMatchingOperations method + * and evaluating the results. + * + * If the implementor is an IComplexOperation, this method will not be + * called automatically. It can be used or left as no-op by the implementor. + * + * @since 18.0.0 + */ + public function onEvent(string $eventName, Event $event, IRuleMatcher $ruleMatcher): void { + $flows = $ruleMatcher->getMatchingOperations(self::class, false); + foreach ($flows as $flow) { + try { + list($mode, $token) = $this->parseOperationConfig($flow['operation']); + $this->validateOperationConfig($mode, $token); + } catch(UnexpectedValueException $e) { + continue; + } + + $room = $this->getRoom($token); + $participant = $this->getParticipant($room); + $this->chatManager->sendMessage( + $room, + $participant, + 'bots', + $participant->getUser(), + 'MESSAGE TODO', + new \DateTime(), + null + ); + } + } + + protected function parseOperationConfig(string $raw): array { + /** + * We expect $operation be a base64 encoded json string, containing + * 't' => string, the room token + * 'm' => int 1..3, the mention-mode (none, yourself, room) + * 'u' => string, the applicable user id + * + * setting up room mentions are only permitted to moderators + */ + + $decoded = base64_decode($raw); + $opConfig = \json_decode($decoded, true); + if(!is_array($opConfig) || empty($opConfig)) { + throw new UnexpectedValueException('Cannot decode operation details'); + } + + $mode = (int)($opConfig['m'] ?? 0); + $token = trim((string)($opConfig['t'] ?? '')); + + return [$mode, $token]; + } + + protected function validateOperationConfig(int $mode, string $token): void { + if(($mode < 1 || $mode > 3)) { + throw new UnexpectedValueException('Invalid mode'); + } + + if(empty($token)) { + throw new UnexpectedValueException('Invalid token'); + } + + try { + $room = $this->getRoom($token); + } catch (RoomNotFoundException $e) { + throw new UnexpectedValueException('Room not found', $e->getCode(), $e); + } + + if($mode === 3) { + try { + $participant = $this->getParticipant($room); + if (!$participant->hasModeratorPermissions(false)) { + throw new UnexpectedValueException('Not allowed to mention room'); + } + } catch (ParticipantNotFoundException $e) { + throw new UnexpectedValueException('Participant not found', $e->getCode(), $e); + } + } + } + + /** + * @throws UnexpectedValueException + */ + protected function getUser(): IUser { + $user = $this->session->getUser(); + if($user === null) { + throw new UnexpectedValueException('User not logged in'); + } + return $user; + } + + /** + * @throws RoomNotFoundException + */ + protected function getRoom(string $token): Room { + $user = $this->getUser(); + return $this->talkManager->getRoomForParticipantByToken($token, $user->getUID()); + } + + /** + * @throws ParticipantNotFoundException + */ + protected function getParticipant(Room $room): Participant { + $user = $this->getUser(); + return $room->getParticipant($user->getUID()); + } +} From c6baf40fa1ca81f5dcd8ad5ae4ee5500de63a9e4 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Fri, 22 Nov 2019 12:57:51 +0100 Subject: [PATCH 02/12] add flow operation, frontend part Signed-off-by: Arthur Schiwon --- lib/Flow/Operation.php | 8 +-- src/PostToConversation.vue | 103 +++++++++++++++++++++++++++++++++++++ src/flow.js | 8 +++ webpack.common.js | 1 + 4 files changed, 116 insertions(+), 4 deletions(-) create mode 100644 src/PostToConversation.vue create mode 100644 src/flow.js diff --git a/lib/Flow/Operation.php b/lib/Flow/Operation.php index 5d0d5a6e49a..a8cdcf89088 100644 --- a/lib/Flow/Operation.php +++ b/lib/Flow/Operation.php @@ -24,6 +24,7 @@ namespace OCA\Talk\Flow; +use OC_Util; use OCA\Talk\Chat\ChatManager; use OCA\Talk\Exceptions\ParticipantNotFoundException; use OCA\Talk\Exceptions\RoomNotFoundException; @@ -73,6 +74,7 @@ public static function register(EventDispatcherInterface $dispatcher): void { $dispatcher->addListener(FlowManager::EVENT_NAME_REG_OPERATION, function (GenericEvent $event) { $operation = \OC::$server->query(Operation::class); $event->getSubject()->registerOperation($operation); + OC_Util::addScript('spreed', 'flow'); }); } @@ -142,16 +144,14 @@ public function onEvent(string $eventName, Event $event, IRuleMatcher $ruleMatch protected function parseOperationConfig(string $raw): array { /** - * We expect $operation be a base64 encoded json string, containing + * We expect $operation be a json string, containing * 't' => string, the room token * 'm' => int 1..3, the mention-mode (none, yourself, room) - * 'u' => string, the applicable user id * * setting up room mentions are only permitted to moderators */ - $decoded = base64_decode($raw); - $opConfig = \json_decode($decoded, true); + $opConfig = \json_decode($raw, true); if(!is_array($opConfig) || empty($opConfig)) { throw new UnexpectedValueException('Cannot decode operation details'); } diff --git a/src/PostToConversation.vue b/src/PostToConversation.vue new file mode 100644 index 00000000000..854c6767045 --- /dev/null +++ b/src/PostToConversation.vue @@ -0,0 +1,103 @@ + + + + + diff --git a/src/flow.js b/src/flow.js new file mode 100644 index 00000000000..ac447ee2761 --- /dev/null +++ b/src/flow.js @@ -0,0 +1,8 @@ +import PostToConversation from './PostToConversation' + +window.OCA.WorkflowEngine.registerOperator({ + id: 'OCA\\Talk\\Flow\\Operation', + color: 'tomato', + operation: '', + options: PostToConversation, +}) diff --git a/webpack.common.js b/webpack.common.js index e20bb387d9f..6e269b0f60b 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -14,6 +14,7 @@ module.exports = { 'talk': path.join(__dirname, 'src', 'main.js'), 'talk-chat-tab': path.join(__dirname, 'src', 'mainChatTab.js'), 'files-sidebar-tab': path.join(__dirname, 'src', 'mainSidebarTab.js'), + 'flow': path.join(__dirname, 'src', 'flow.js') }, output: { path: path.resolve(__dirname, './js'), From 34537dfcc448779a9ce0dd3fba027019eee67551 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Mon, 25 Nov 2019 18:04:09 +0100 Subject: [PATCH 03/12] prepare mention of the message Signed-off-by: Arthur Schiwon --- lib/Flow/Operation.php | 30 ++++++++++++++++++++++++++---- src/PostToConversation.vue | 1 + 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/lib/Flow/Operation.php b/lib/Flow/Operation.php index a8cdcf89088..582b8f968ef 100644 --- a/lib/Flow/Operation.php +++ b/lib/Flow/Operation.php @@ -45,6 +45,13 @@ class Operation implements IOperation { + /** @var int[] */ + public const MESSAGE_MODES = [ + 'NO_MENTION' => 1, + 'SELF_MENTION' => 2, + 'ROOM_MENTION' => 3, + ]; + /** @var IL10N */ private $l; /** @var IURLGenerator */ @@ -135,18 +142,33 @@ public function onEvent(string $eventName, Event $event, IRuleMatcher $ruleMatch $participant, 'bots', $participant->getUser(), - 'MESSAGE TODO', + $this->prepareMention($mode, $participant) . 'MESSAGE TODO', new \DateTime(), null ); } } + /** + * returns a mention including a trailing whitespace, or an empty string + */ + protected function prepareMention(int $mode, Participant $participant): string { + switch ($mode) { + case self::MESSAGE_MODES['ROOM_MENTION']: + return '@all '; + case self::MESSAGE_MODES['SELF_MENTION']: + return '@"' . $participant->getUser() . '" '; + case self::MESSAGE_MODES['NO_MENTION']: + default: + return ''; + } + } + protected function parseOperationConfig(string $raw): array { /** * We expect $operation be a json string, containing * 't' => string, the room token - * 'm' => int 1..3, the mention-mode (none, yourself, room) + * 'm' => int > 0, see self::MESSAGE_MODES * * setting up room mentions are only permitted to moderators */ @@ -163,7 +185,7 @@ protected function parseOperationConfig(string $raw): array { } protected function validateOperationConfig(int $mode, string $token): void { - if(($mode < 1 || $mode > 3)) { + if(!in_array($mode, self::MESSAGE_MODES)) { throw new UnexpectedValueException('Invalid mode'); } @@ -177,7 +199,7 @@ protected function validateOperationConfig(int $mode, string $token): void { throw new UnexpectedValueException('Room not found', $e->getCode(), $e); } - if($mode === 3) { + if($mode === self::MESSAGE_MODES['ROOM_MENTION']) { try { $participant = $this->getParticipant($room); if (!$participant->hasModeratorPermissions(false)) { diff --git a/src/PostToConversation.vue b/src/PostToConversation.vue index 854c6767045..79ffd0127ff 100644 --- a/src/PostToConversation.vue +++ b/src/PostToConversation.vue @@ -18,6 +18,7 @@ import Multiselect from '@nextcloud/vue/dist/Components/Multiselect' import axios from '@nextcloud/axios' +// see \OCA\Talk\Flow\Operation::MESSAGE_MODES const conversationModeOptions = [ { id: 1, From 8e042b537e5d1fdefe7ffa4985f8c49fab4f0bf5 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Tue, 26 Nov 2019 14:55:26 +0100 Subject: [PATCH 04/12] catch Talk exceptions Signed-off-by: Arthur Schiwon --- lib/Flow/Operation.php | 40 ++++++++++++++++------------------------ 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/lib/Flow/Operation.php b/lib/Flow/Operation.php index 582b8f968ef..c20825ab34a 100644 --- a/lib/Flow/Operation.php +++ b/lib/Flow/Operation.php @@ -113,39 +113,31 @@ public function validateOperation(string $name, array $checks, string $operation $this->validateOperationConfig($mode, $token); } - /** - * Is being called by the workflow engine when an event was triggered that - * is configured for this operation. An evaluation whether the event - * qualifies for this operation to run has still to be done by the - * implementor by calling the RuleMatchers getMatchingOperations method - * and evaluating the results. - * - * If the implementor is an IComplexOperation, this method will not be - * called automatically. It can be used or left as no-op by the implementor. - * - * @since 18.0.0 - */ public function onEvent(string $eventName, Event $event, IRuleMatcher $ruleMatcher): void { $flows = $ruleMatcher->getMatchingOperations(self::class, false); foreach ($flows as $flow) { try { list($mode, $token) = $this->parseOperationConfig($flow['operation']); $this->validateOperationConfig($mode, $token); + + $room = $this->getRoom($token); + $participant = $this->getParticipant($room); + $this->chatManager->sendMessage( + $room, + $participant, + 'bots', + $participant->getUser(), + $this->prepareMention($mode, $participant) . 'MESSAGE TODO', + new \DateTime(), + null + ); } catch(UnexpectedValueException $e) { continue; + } catch (ParticipantNotFoundException $e) { + continue; + } catch (RoomNotFoundException $e) { + continue; } - - $room = $this->getRoom($token); - $participant = $this->getParticipant($room); - $this->chatManager->sendMessage( - $room, - $participant, - 'bots', - $participant->getUser(), - $this->prepareMention($mode, $participant) . 'MESSAGE TODO', - new \DateTime(), - null - ); } } From a0dd6ef8bc0d8065cc3aea6b3bfbc678d33dba63 Mon Sep 17 00:00:00 2001 From: Arthur Schiwon Date: Tue, 26 Nov 2019 15:33:32 +0100 Subject: [PATCH 05/12] move mode constants to constants Signed-off-by: Arthur Schiwon --- src/PostToConversation.vue | 46 ++++++++++++++++---------------------- src/constants.js | 7 ++++++ 2 files changed, 26 insertions(+), 27 deletions(-) diff --git a/src/PostToConversation.vue b/src/PostToConversation.vue index 79ffd0127ff..73fcfa104e1 100644 --- a/src/PostToConversation.vue +++ b/src/PostToConversation.vue @@ -17,22 +17,7 @@