Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/spreed.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJoas Schilling <213943+nickvergessen@users.noreply.github.com>2019-12-16 17:24:27 +0300
committerGitHub <noreply@github.com>2019-12-16 17:24:27 +0300
commit32049dc1c3da8ce804a89563b74ca0f1d6e6cd25 (patch)
tree90a0cdfa6e3c938494e0d97ceb05c55feffe74e0
parent925cb5f2b22d77f54e017a53e2855b3bc34f98e2 (diff)
parent2a87d67b7bfa9a7eb6d90607d36f2a59f16a2fc1 (diff)
Merge pull request #2487 from nextcloud/enh/2414/flow-operator
add post to conversation flow operator
-rw-r--r--lib/AppInfo/Application.php2
-rw-r--r--lib/Flow/Operation.php254
-rw-r--r--src/constants.js7
-rw-r--r--src/flow.js30
-rw-r--r--src/views/FlowPostToConversation.vue96
-rw-r--r--webpack.common.js1
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'),