You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

256 lines
7.2 KiB

  1. <?php
  2. declare(strict_types=1);
  3. /**
  4. * @copyright Copyright (c) 2019 Arthur Schiwon <blizzz@arthur-schiwon.de>
  5. *
  6. * @author Arthur Schiwon <blizzz@arthur-schiwon.de>
  7. *
  8. * @license GNU AGPL version 3 or any later version
  9. *
  10. * This program is free software: you can redistribute it and/or modify
  11. * it under the terms of the GNU Affero General Public License as
  12. * published by the Free Software Foundation, either version 3 of the
  13. * License, or (at your option) any later version.
  14. *
  15. * This program is distributed in the hope that it will be useful,
  16. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  17. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  18. * GNU Affero General Public License for more details.
  19. *
  20. * You should have received a copy of the GNU Affero General Public License
  21. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  22. *
  23. */
  24. namespace OCA\Talk\Flow;
  25. use OCA\Talk\Chat\ChatManager;
  26. use OCA\Talk\Exceptions\ParticipantNotFoundException;
  27. use OCA\Talk\Exceptions\RoomNotFoundException;
  28. use OCA\Talk\Manager as TalkManager;
  29. use OCA\Talk\Participant;
  30. use OCA\Talk\Room;
  31. use OCP\EventDispatcher\Event;
  32. use OCP\EventDispatcher\IEventDispatcher;
  33. use OCP\IL10N;
  34. use OCP\IURLGenerator;
  35. use OCP\IUser;
  36. use OCP\IUserSession;
  37. use OCP\Util;
  38. use OCP\WorkflowEngine\EntityContext\IDisplayText;
  39. use OCP\WorkflowEngine\EntityContext\IUrl;
  40. use OCP\WorkflowEngine\IEntity;
  41. use OCP\WorkflowEngine\IManager as FlowManager;
  42. use OCP\WorkflowEngine\IOperation;
  43. use OCP\WorkflowEngine\IRuleMatcher;
  44. use Symfony\Component\EventDispatcher\GenericEvent;
  45. use UnexpectedValueException;
  46. class Operation implements IOperation {
  47. /** @var int[] */
  48. public const MESSAGE_MODES = [
  49. 'NO_MENTION' => 1,
  50. 'SELF_MENTION' => 2,
  51. 'ROOM_MENTION' => 3,
  52. ];
  53. /** @var IL10N */
  54. private $l;
  55. /** @var IURLGenerator */
  56. private $urlGenerator;
  57. /** @var TalkManager */
  58. private $talkManager;
  59. /** @var IUserSession */
  60. private $session;
  61. /** @var ChatManager */
  62. private $chatManager;
  63. public function __construct(
  64. IL10N $l,
  65. IURLGenerator $urlGenerator,
  66. TalkManager $talkManager,
  67. IUserSession $session,
  68. ChatManager $chatManager
  69. ) {
  70. $this->l = $l;
  71. $this->urlGenerator = $urlGenerator;
  72. $this->talkManager = $talkManager;
  73. $this->session = $session;
  74. $this->chatManager = $chatManager;
  75. }
  76. public static function register(IEventDispatcher $dispatcher): void {
  77. $dispatcher->addListener(FlowManager::EVENT_NAME_REG_OPERATION, function (GenericEvent $event) {
  78. $operation = \OC::$server->query(Operation::class);
  79. $event->getSubject()->registerOperation($operation);
  80. Util::addScript('spreed', 'flow');
  81. });
  82. }
  83. public function getDisplayName(): string {
  84. return $this->l->t('Write to conversation');
  85. }
  86. public function getDescription(): string {
  87. return $this->l->t('Writes event information into a conversation of your choice');
  88. }
  89. public function getIcon(): string {
  90. return $this->urlGenerator->imagePath('spreed', 'app.svg');
  91. }
  92. public function isAvailableForScope(int $scope): bool {
  93. return $scope === FlowManager::SCOPE_USER;
  94. }
  95. /**
  96. * Validates whether a configured workflow rule is valid. If it is not,
  97. * an `\UnexpectedValueException` is supposed to be thrown.
  98. *
  99. * @throws UnexpectedValueException
  100. * @since 9.1
  101. */
  102. public function validateOperation(string $name, array $checks, string $operation): void {
  103. list($mode, $token) = $this->parseOperationConfig($operation);
  104. $this->validateOperationConfig($mode, $token, $this->getUser()->getUID());
  105. }
  106. public function onEvent(string $eventName, Event $event, IRuleMatcher $ruleMatcher): void {
  107. $flows = $ruleMatcher->getFlows(false);
  108. foreach ($flows as $flow) {
  109. try {
  110. list($mode, $token) = $this->parseOperationConfig($flow['operation']);
  111. $uid = $flow['scope_actor_id'];
  112. $this->validateOperationConfig($mode, $token, $uid);
  113. $entity = $ruleMatcher->getEntity();
  114. $message = $this->prepareText($entity, $eventName);
  115. if ($message === '') {
  116. continue;
  117. }
  118. $room = $this->getRoom($token, $uid);
  119. $participant = $this->getParticipant($uid, $room);
  120. $this->chatManager->sendMessage(
  121. $room,
  122. $participant,
  123. 'bots',
  124. $participant->getUser(),
  125. $this->prepareMention($mode, $participant) . $message,
  126. new \DateTime(),
  127. null,
  128. ''
  129. );
  130. } catch (UnexpectedValueException $e) {
  131. continue;
  132. } catch (ParticipantNotFoundException $e) {
  133. continue;
  134. } catch (RoomNotFoundException $e) {
  135. continue;
  136. }
  137. }
  138. }
  139. protected function prepareText(IEntity $entity, string $eventName) {
  140. $message = $eventName;
  141. if ($entity instanceof IDisplayText) {
  142. $message = trim($entity->getDisplayText(3));
  143. }
  144. if ($entity instanceof IUrl && $message !== '') {
  145. $message .= ' ' . $entity->getUrl();
  146. }
  147. return $message;
  148. }
  149. /**
  150. * returns a mention including a trailing whitespace, or an empty string
  151. */
  152. protected function prepareMention(int $mode, Participant $participant): string {
  153. switch ($mode) {
  154. case self::MESSAGE_MODES['ROOM_MENTION']:
  155. return '@all ';
  156. case self::MESSAGE_MODES['SELF_MENTION']:
  157. $hasWhitespace = strpos($participant->getUser(), ' ') !== false;
  158. $enclosure = $hasWhitespace ? '"' : '';
  159. return '@' . $enclosure . $participant->getUser() . $enclosure . ' ';
  160. case self::MESSAGE_MODES['NO_MENTION']:
  161. default:
  162. return '';
  163. }
  164. }
  165. protected function parseOperationConfig(string $raw): array {
  166. /**
  167. * We expect $operation be a json string, containing
  168. * 't' => string, the room token
  169. * 'm' => int > 0, see self::MESSAGE_MODES
  170. *
  171. * setting up room mentions are only permitted to moderators
  172. */
  173. $opConfig = \json_decode($raw, true);
  174. if (!is_array($opConfig) || empty($opConfig)) {
  175. throw new UnexpectedValueException('Cannot decode operation details');
  176. }
  177. $mode = (int)($opConfig['m'] ?? 0);
  178. $token = trim((string)($opConfig['t'] ?? ''));
  179. return [$mode, $token];
  180. }
  181. protected function validateOperationConfig(int $mode, string $token, string $uid): void {
  182. if (!in_array($mode, self::MESSAGE_MODES)) {
  183. throw new UnexpectedValueException('Invalid mode');
  184. }
  185. if (empty($token)) {
  186. throw new UnexpectedValueException('Invalid token');
  187. }
  188. try {
  189. $room = $this->getRoom($token, $uid);
  190. } catch (RoomNotFoundException $e) {
  191. throw new UnexpectedValueException('Room not found', $e->getCode(), $e);
  192. }
  193. if ($mode === self::MESSAGE_MODES['ROOM_MENTION']) {
  194. try {
  195. $participant = $this->getParticipant($uid, $room);
  196. if (!$participant->hasModeratorPermissions(false)) {
  197. throw new UnexpectedValueException('Not allowed to mention room');
  198. }
  199. } catch (ParticipantNotFoundException $e) {
  200. throw new UnexpectedValueException('Participant not found', $e->getCode(), $e);
  201. }
  202. }
  203. }
  204. /**
  205. * @throws UnexpectedValueException
  206. */
  207. protected function getUser(): IUser {
  208. $user = $this->session->getUser();
  209. if ($user === null) {
  210. throw new UnexpectedValueException('User not logged in');
  211. }
  212. return $user;
  213. }
  214. /**
  215. * @throws RoomNotFoundException
  216. */
  217. protected function getRoom(string $token, string $uid): Room {
  218. return $this->talkManager->getRoomForParticipantByToken($token, $uid);
  219. }
  220. /**
  221. * @throws ParticipantNotFoundException
  222. */
  223. protected function getParticipant(string $uid, Room $room): Participant {
  224. return $room->getParticipant($uid);
  225. }
  226. }