Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add post to conversation flow operator #2487

Merged
merged 12 commits into from
Dec 16, 2019
2 changes: 2 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
254 changes: 254 additions & 0 deletions lib/Flow/Operation.php
Original file line number Diff line number Diff line change
@@ -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;
}
blizzz marked this conversation as resolved.
Show resolved Hide resolved

$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');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this intended? we do not limit mentions of the "conversation" to moderators

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We agreed that we do not want a person be able to spam others with mentions. Now I had the idea that some trusted individuals it might be allowed to trigger mentions to a crowd, and in Talk they would be identified by Moderators. If it does not make sense, we can kill it. Or extend/relax it later.

}
} 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);
}
}
7 changes: 7 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,10 @@ export const SHARE = {
CIRCLE: 7,
},
}
export const FLOW = {
MESSAGE_MODES: {
NO_MENTION: 1,
SELF_MENTION: 2,
ROOM_MENTION: 3,
},
}
30 changes: 30 additions & 0 deletions src/flow.js
Original file line number Diff line number Diff line change
@@ -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,
})
Loading