diff options
author | Joas Schilling <213943+nickvergessen@users.noreply.github.com> | 2019-12-16 17:24:27 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-12-16 17:24:27 +0300 |
commit | 32049dc1c3da8ce804a89563b74ca0f1d6e6cd25 (patch) | |
tree | 90a0cdfa6e3c938494e0d97ceb05c55feffe74e0 | |
parent | 925cb5f2b22d77f54e017a53e2855b3bc34f98e2 (diff) | |
parent | 2a87d67b7bfa9a7eb6d90607d36f2a59f16a2fc1 (diff) |
Merge pull request #2487 from nextcloud/enh/2414/flow-operator
add post to conversation flow operator
-rw-r--r-- | lib/AppInfo/Application.php | 2 | ||||
-rw-r--r-- | lib/Flow/Operation.php | 254 | ||||
-rw-r--r-- | src/constants.js | 7 | ||||
-rw-r--r-- | src/flow.js | 30 | ||||
-rw-r--r-- | src/views/FlowPostToConversation.vue | 96 | ||||
-rw-r--r-- | webpack.common.js | 1 |
6 files changed, 390 insertions, 0 deletions
diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index af2694eb6..07bebdb59 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -37,6 +37,7 @@ use OCA\Talk\Events\ChatEvent; 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 @@ class Application extends App { 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 000000000..5ed95dba1 --- /dev/null +++ b/lib/Flow/Operation.php @@ -0,0 +1,254 @@ +<?php +declare(strict_types=1); +/** + * @copyright Copyright (c) 2019 Arthur Schiwon <blizzz@arthur-schiwon.de> + * + * @author Arthur Schiwon <blizzz@arthur-schiwon.de> + * + * @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\Talk\Flow; + +use OC_Util; +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\EventDispatcher\IEventDispatcher; +use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\IUser; +use OCP\IUserSession; +use OCP\WorkflowEngine\EntityContext\IDisplayText; +use OCP\WorkflowEngine\EntityContext\IUrl; +use OCP\WorkflowEngine\IEntity; +use OCP\WorkflowEngine\IManager as FlowManager; +use OCP\WorkflowEngine\IOperation; +use OCP\WorkflowEngine\IRuleMatcher; +use Symfony\Component\EventDispatcher\GenericEvent; +use UnexpectedValueException; + +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 */ + 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(IEventDispatcher $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'); + }); + } + + 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, $this->getUser()->getUID()); + } + + public function onEvent(string $eventName, Event $event, IRuleMatcher $ruleMatcher): void { + $flows = $ruleMatcher->getFlows(false); + foreach ($flows as $flow) { + try { + list($mode, $token) = $this->parseOperationConfig($flow['operation']); + $uid = $flow['scope_actor_id']; + $this->validateOperationConfig($mode, $token, $uid); + + $entity = $ruleMatcher->getEntity(); + + $message = $this->prepareText($entity, $eventName); + if($message === '') { + continue; + } + + $room = $this->getRoom($token, $uid); + $participant = $this->getParticipant($uid, $room); + $this->chatManager->sendMessage( + $room, + $participant, + 'bots', + $participant->getUser(), + $this->prepareMention($mode, $participant) . $message, + new \DateTime(), + null + ); + } catch (UnexpectedValueException $e) { + continue; + } catch (ParticipantNotFoundException $e) { + continue; + } catch (RoomNotFoundException $e) { + continue; + } + } + } + + protected function prepareText(IEntity $entity, string $eventName) { + $message = $eventName; + if($entity instanceof IDisplayText) { + $message = trim($entity->getDisplayText(3)); + } + if($entity instanceof IUrl && $message !== '') { + $message .= ' ' . $entity->getUrl(); + } + return $message; + } + + /** + * 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']: + $hasWhitespace = strpos($participant->getUser(), ' ') !== false; + $enclosure = $hasWhitespace ? '"' : ''; + return '@' . $enclosure . $participant->getUser() . $enclosure . ' '; + 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 > 0, see self::MESSAGE_MODES + * + * setting up room mentions are only permitted to moderators + */ + + $opConfig = \json_decode($raw, 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, string $uid): void { + if(!in_array($mode, self::MESSAGE_MODES)) { + throw new UnexpectedValueException('Invalid mode'); + } + + if(empty($token)) { + throw new UnexpectedValueException('Invalid token'); + } + + try { + $room = $this->getRoom($token, $uid); + } catch (RoomNotFoundException $e) { + throw new UnexpectedValueException('Room not found', $e->getCode(), $e); + } + + if($mode === self::MESSAGE_MODES['ROOM_MENTION']) { + try { + $participant = $this->getParticipant($uid, $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, string $uid): Room { + return $this->talkManager->getRoomForParticipantByToken($token, $uid); + } + + /** + * @throws ParticipantNotFoundException + */ + protected function getParticipant(string $uid, Room $room): Participant { + return $room->getParticipant($uid); + } +} diff --git a/src/constants.js b/src/constants.js index c3f83a623..6a56f20fd 100644 --- a/src/constants.js +++ b/src/constants.js @@ -71,3 +71,10 @@ export const SHARE = { CIRCLE: 7, }, } +export const FLOW = { + MESSAGE_MODES: { + NO_MENTION: 1, + SELF_MENTION: 2, + ROOM_MENTION: 3, + }, +} diff --git a/src/flow.js b/src/flow.js new file mode 100644 index 000000000..67860a4e0 --- /dev/null +++ b/src/flow.js @@ -0,0 +1,30 @@ +/** + * @copyright Copyright (c) 2019 Arthur Schiwon <blizzz@arthur-schiwon.de> + * + * @author Arthur Schiwon <blizzz@arthur-schiwon.de> + * + * @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/>. + * + */ + +import FlowPostToConversation from './views/FlowPostToConversation' + +window.OCA.WorkflowEngine.registerOperator({ + id: 'OCA\\Talk\\Flow\\Operation', + color: 'tomato', + operation: '', + options: FlowPostToConversation, +}) diff --git a/src/views/FlowPostToConversation.vue b/src/views/FlowPostToConversation.vue new file mode 100644 index 000000000..35a6ddc0a --- /dev/null +++ b/src/views/FlowPostToConversation.vue @@ -0,0 +1,96 @@ +<template> + <div> + <Multiselect :value="currentRoom" + :options="roomOptions" + track-by="token" + label="displayName" + @input="(newValue) => newValue !== null && $emit('input', JSON.stringify({'m': currentMode.id, 't': newValue.token }))" /> + + <Multiselect :value="currentMode" + :options="modeOptions" + track-by="id" + label="text" + @input="(newValue) => newValue !== null && $emit('input', JSON.stringify({'m': newValue.id, 't': currentRoom.token }))" /> + </div> +</template> + +<script> +import Multiselect from '@nextcloud/vue/dist/Components/Multiselect' +import axios from '@nextcloud/axios' +import { FLOW, CONVERSATION } from '../constants' + +export default { + name: 'FlowPostToConversation', + components: { Multiselect }, + props: { + value: { + default: JSON.stringify({ 'm': '0', 't': '' }), + type: String, + }, + }, + data() { + return { + modeOptions: [ + { + id: FLOW.MESSAGE_MODES.NO_MENTION, + text: t('spreed', 'Message without mention'), + }, + { + id: FLOW.MESSAGE_MODES.SELF_MENTION, + text: t('spreed', 'Mention myself'), + }, + { + id: FLOW.MESSAGE_MODES.ROOM_MENTION, + text: t('spreed', 'Mention room'), + }, + ], + roomOptions: [], + } + }, + computed: { + currentRoom() { + if (this.value === '') { + return '' + } + const selectedRoom = JSON.parse(this.value).t + const newValue = this.roomOptions.find(option => option.token === selectedRoom) + if (typeof newValue === 'undefined') { + return '' + } + return newValue + }, + currentMode() { + if (this.value === '') { + return this.modeOptions[0] + } + const selectedMode = JSON.parse(this.value).m + const newValue = this.modeOptions.find(option => option.id === selectedMode) + if (typeof newValue === 'undefined') { + return this.modeOptions[0] + } + return newValue + }, + }, + beforeMount() { + this.fetchRooms() + }, + methods: { + fetchRooms() { + axios.get(OC.linkToOCS('/apps/spreed/api/v1', 2) + 'room').then((response) => { + this.roomOptions = response.data.ocs.data.filter(function(room) { + return room.readOnly === CONVERSATION.STATE.READ_WRITE + }) + }) + }, + }, +} + +</script> + +<style scoped> + .multiselect { + width: 100%; + margin: auto; + text-align: center; + } +</style> diff --git a/webpack.common.js b/webpack.common.js index f3d5a2f07..958f533c0 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'), |