1<?php 2 3declare(strict_types=1); 4/** 5 * @copyright Copyright (c) 2018 Joas Schilling <coding@schilljs.com> 6 * 7 * @license GNU AGPL version 3 or any later version 8 * 9 * This program is free software: you can redistribute it and/or modify 10 * it under the terms of the GNU Affero General Public License as 11 * published by the Free Software Foundation, either version 3 of the 12 * License, or (at your option) any later version. 13 * 14 * This program is distributed in the hope that it will be useful, 15 * but WITHOUT ANY WARRANTY; without even the implied warranty of 16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 * GNU Affero General Public License for more details. 18 * 19 * You should have received a copy of the GNU Affero General Public License 20 * along with this program. If not, see <http://www.gnu.org/licenses/>. 21 * 22 */ 23 24namespace OCA\Talk\Chat\SystemMessage; 25 26use OCA\Talk\Chat\ChatManager; 27use OCA\Talk\Events\AddParticipantsEvent; 28use OCA\Talk\Events\AttendeesAddedEvent; 29use OCA\Talk\Events\AttendeesRemovedEvent; 30use OCA\Talk\Events\ModifyEveryoneEvent; 31use OCA\Talk\Events\ModifyLobbyEvent; 32use OCA\Talk\Events\ModifyParticipantEvent; 33use OCA\Talk\Events\ModifyRoomEvent; 34use OCA\Talk\Events\RemoveUserEvent; 35use OCA\Talk\Events\RoomEvent; 36use OCA\Talk\Manager; 37use OCA\Talk\Model\Attendee; 38use OCA\Talk\Model\Session; 39use OCA\Talk\Participant; 40use OCA\Talk\Room; 41use OCA\Talk\Service\ParticipantService; 42use OCA\Talk\Share\RoomShareProvider; 43use OCA\Talk\TalkSession; 44use OCA\Talk\Webinary; 45use OCP\AppFramework\Utility\ITimeFactory; 46use OCP\EventDispatcher\Event; 47use OCP\EventDispatcher\IEventDispatcher; 48use OCP\EventDispatcher\IEventListener; 49use OCP\IRequest; 50use OCP\ISession; 51use OCP\IUser; 52use OCP\IUserSession; 53use OCP\Share\IShare; 54use Symfony\Component\EventDispatcher\GenericEvent; 55 56class Listener implements IEventListener { 57 58 /** @var IRequest */ 59 protected $request; 60 /** @var ChatManager */ 61 protected $chatManager; 62 /** @var TalkSession */ 63 protected $talkSession; 64 /** @var ISession */ 65 protected $session; 66 /** @var IUserSession */ 67 protected $userSession; 68 /** @var ITimeFactory */ 69 protected $timeFactory; 70 71 public function __construct(IRequest $request, 72 ChatManager $chatManager, 73 TalkSession $talkSession, 74 ISession $session, 75 IUserSession $userSession, 76 ITimeFactory $timeFactory) { 77 $this->request = $request; 78 $this->chatManager = $chatManager; 79 $this->talkSession = $talkSession; 80 $this->session = $session; 81 $this->userSession = $userSession; 82 $this->timeFactory = $timeFactory; 83 } 84 85 public static function register(IEventDispatcher $dispatcher): void { 86 $dispatcher->addListener(Room::EVENT_BEFORE_SESSION_JOIN_CALL, static function (ModifyParticipantEvent $event) { 87 $room = $event->getRoom(); 88 /** @var self $listener */ 89 $listener = \OC::$server->query(self::class); 90 /** @var ParticipantService $participantService */ 91 $participantService = \OC::$server->query(ParticipantService::class); 92 93 if ($participantService->hasActiveSessionsInCall($room)) { 94 $listener->sendSystemMessage($room, 'call_joined', [], $event->getParticipant()); 95 } else { 96 $listener->sendSystemMessage($room, 'call_started', [], $event->getParticipant()); 97 } 98 }); 99 $dispatcher->addListener(Room::EVENT_AFTER_SESSION_LEAVE_CALL, static function (ModifyParticipantEvent $event) { 100 if ($event instanceof ModifyEveryoneEvent) { 101 // No individual system message if the call is ended for everyone 102 return; 103 } 104 105 if ($event->getNewValue() === $event->getOldValue()) { 106 return; 107 } 108 109 $room = $event->getRoom(); 110 111 $session = $event->getParticipant()->getSession(); 112 if (!$session instanceof Session) { 113 // This happens in case the user was kicked/lobbied 114 return; 115 } 116 117 /** @var self $listener */ 118 $listener = \OC::$server->query(self::class); 119 120 $listener->sendSystemMessage($room, 'call_left', [], $event->getParticipant()); 121 }); 122 123 $dispatcher->addListener(Room::EVENT_AFTER_ROOM_CREATE, static function (RoomEvent $event) { 124 $room = $event->getRoom(); 125 /** @var self $listener */ 126 $listener = \OC::$server->query(self::class); 127 128 $listener->sendSystemMessage($room, 'conversation_created'); 129 }); 130 $dispatcher->addListener(Room::EVENT_AFTER_NAME_SET, static function (ModifyRoomEvent $event) { 131 if ($event->getOldValue() === '' || 132 $event->getNewValue() === '') { 133 return; 134 } 135 136 $room = $event->getRoom(); 137 /** @var self $listener */ 138 $listener = \OC::$server->query(self::class); 139 140 $listener->sendSystemMessage($room, 'conversation_renamed', [ 141 'newName' => $event->getNewValue(), 142 'oldName' => $event->getOldValue(), 143 ]); 144 }); 145 $dispatcher->addListener(Room::EVENT_AFTER_DESCRIPTION_SET, static function (ModifyRoomEvent $event) { 146 $room = $event->getRoom(); 147 /** @var self $listener */ 148 $listener = \OC::$server->get(self::class); 149 150 if ($event->getNewValue() !== '') { 151 $listener->sendSystemMessage($room, 'description_set', [ 152 'newDescription' => $event->getNewValue(), 153 ]); 154 } else { 155 $listener->sendSystemMessage($room, 'description_removed'); 156 } 157 }); 158 $dispatcher->addListener(Room::EVENT_AFTER_PASSWORD_SET, static function (ModifyRoomEvent $event) { 159 $room = $event->getRoom(); 160 /** @var self $listener */ 161 $listener = \OC::$server->query(self::class); 162 163 if ($event->getNewValue() !== '') { 164 $listener->sendSystemMessage($room, 'password_set'); 165 } else { 166 $listener->sendSystemMessage($room, 'password_removed'); 167 } 168 }); 169 $dispatcher->addListener(Room::EVENT_AFTER_TYPE_SET, static function (ModifyRoomEvent $event) { 170 $room = $event->getRoom(); 171 172 if ($event->getOldValue() === Room::TYPE_ONE_TO_ONE) { 173 return; 174 } 175 176 if ($event->getNewValue() === Room::TYPE_PUBLIC) { 177 /** @var self $listener */ 178 $listener = \OC::$server->query(self::class); 179 $listener->sendSystemMessage($room, 'guests_allowed'); 180 } elseif ($event->getNewValue() === Room::TYPE_GROUP) { 181 /** @var self $listener */ 182 $listener = \OC::$server->query(self::class); 183 $listener->sendSystemMessage($room, 'guests_disallowed'); 184 } 185 }); 186 $dispatcher->addListener(Room::EVENT_AFTER_READONLY_SET, static function (ModifyRoomEvent $event) { 187 $room = $event->getRoom(); 188 189 if ($room->getType() === Room::TYPE_CHANGELOG) { 190 return; 191 } 192 193 /** @var self $listener */ 194 $listener = \OC::$server->query(self::class); 195 196 if ($event->getNewValue() === Room::READ_ONLY) { 197 $listener->sendSystemMessage($room, 'read_only'); 198 } elseif ($event->getNewValue() === Room::READ_WRITE) { 199 $listener->sendSystemMessage($room, 'read_only_off'); 200 } 201 }); 202 $dispatcher->addListener(Room::EVENT_AFTER_LISTABLE_SET, static function (ModifyRoomEvent $event) { 203 $room = $event->getRoom(); 204 205 /** @var self $listener */ 206 $listener = \OC::$server->query(self::class); 207 208 if ($event->getNewValue() === Room::LISTABLE_NONE) { 209 $listener->sendSystemMessage($room, 'listable_none'); 210 } elseif ($event->getNewValue() === Room::LISTABLE_USERS) { 211 $listener->sendSystemMessage($room, 'listable_users'); 212 } elseif ($event->getNewValue() === Room::LISTABLE_ALL) { 213 $listener->sendSystemMessage($room, 'listable_all'); 214 } 215 }); 216 $dispatcher->addListener(Room::EVENT_AFTER_LOBBY_STATE_SET, static function (ModifyLobbyEvent $event) { 217 if ($event->getNewValue() === $event->getOldValue()) { 218 return; 219 } 220 221 $room = $event->getRoom(); 222 223 /** @var self $listener */ 224 $listener = \OC::$server->query(self::class); 225 226 if ($event->isTimerReached()) { 227 $listener->sendSystemMessage($room, 'lobby_timer_reached'); 228 } elseif ($event->getNewValue() === Webinary::LOBBY_NONE) { 229 $listener->sendSystemMessage($room, 'lobby_none'); 230 } elseif ($event->getNewValue() === Webinary::LOBBY_NON_MODERATORS) { 231 $listener->sendSystemMessage($room, 'lobby_non_moderators'); 232 } 233 }); 234 235 $dispatcher->addListener(Room::EVENT_AFTER_USERS_ADD, static function (AddParticipantsEvent $event) { 236 $room = $event->getRoom(); 237 if ($room->getType() === Room::TYPE_ONE_TO_ONE) { 238 return; 239 } 240 241 /** @var self $listener */ 242 $listener = \OC::$server->query(self::class); 243 244 $participants = $event->getParticipants(); 245 246 foreach ($participants as $participant) { 247 if ($participant['actorType'] !== 'users') { 248 continue; 249 } 250 251 $participantType = null; 252 if (isset($participant['participantType'])) { 253 $participantType = $participant['participantType']; 254 } 255 256 $userJoinedFileRoom = $room->getObjectType() === 'file' && $participantType !== Participant::USER_SELF_JOINED; 257 258 // add a message "X joined the conversation", whenever user $userId: 259 if ( 260 // - has joined a file room but not through a public link 261 $userJoinedFileRoom 262 // - has been added by another user (and not when creating a conversation) 263 || $listener->getUserId() !== $participant['actorId'] 264 // - has joined a listable room on their own 265 || $participantType === Participant::USER) { 266 $listener->sendSystemMessage($room, 'user_added', ['user' => $participant['actorId']]); 267 } 268 } 269 }); 270 $dispatcher->addListener(Room::EVENT_AFTER_USER_REMOVE, static function (RemoveUserEvent $event) { 271 $room = $event->getRoom(); 272 273 if ($room->getType() === Room::TYPE_ONE_TO_ONE) { 274 return; 275 } 276 277 if ($event->getReason() === Room::PARTICIPANT_LEFT 278 && $event->getParticipant()->getAttendee()->getParticipantType() === Participant::USER_SELF_JOINED) { 279 // Self-joined user closes the tab/window or leaves via the menu 280 return; 281 } 282 283 /** @var self $listener */ 284 $listener = \OC::$server->query(self::class); 285 $listener->sendSystemMessage($room, 'user_removed', ['user' => $event->getUser()->getUID()]); 286 }); 287 $dispatcher->addListener(Room::EVENT_AFTER_PARTICIPANT_TYPE_SET, static function (ModifyParticipantEvent $event) { 288 $room = $event->getRoom(); 289 $attendee = $event->getParticipant()->getAttendee(); 290 291 if ($attendee->getActorType() !== Attendee::ACTOR_USERS && $attendee->getActorType() !== Attendee::ACTOR_GUESTS) { 292 return; 293 } 294 295 if ($event->getNewValue() === Participant::MODERATOR) { 296 /** @var self $listener */ 297 $listener = \OC::$server->query(self::class); 298 $listener->sendSystemMessage($room, 'moderator_promoted', ['user' => $attendee->getActorId()]); 299 } elseif ($event->getNewValue() === Participant::USER) { 300 if ($event->getOldValue() === Participant::USER_SELF_JOINED) { 301 /** @var self $listener */ 302 $listener = \OC::$server->query(self::class); 303 $listener->sendSystemMessage($room, 'user_added', ['user' => $attendee->getActorId()]); 304 } else { 305 /** @var self $listener */ 306 $listener = \OC::$server->query(self::class); 307 $listener->sendSystemMessage($room, 'moderator_demoted', ['user' => $attendee->getActorId()]); 308 } 309 } elseif ($event->getNewValue() === Participant::GUEST_MODERATOR) { 310 /** @var self $listener */ 311 $listener = \OC::$server->query(self::class); 312 $listener->sendSystemMessage($room, 'guest_moderator_promoted', ['session' => $attendee->getActorId()]); 313 } elseif ($event->getNewValue() === Participant::GUEST) { 314 /** @var self $listener */ 315 $listener = \OC::$server->query(self::class); 316 $listener->sendSystemMessage($room, 'guest_moderator_demoted', ['session' => $attendee->getActorId()]); 317 } 318 }); 319 $listener = function (GenericEvent $event): void { 320 /** @var IShare $share */ 321 $share = $event->getSubject(); 322 323 if ($share->getShareType() !== IShare::TYPE_ROOM) { 324 return; 325 } 326 327 /** @var self $listener */ 328 $listener = \OC::$server->query(self::class); 329 330 /** @var Manager $manager */ 331 $manager = \OC::$server->query(Manager::class); 332 333 $room = $manager->getRoomByToken($share->getSharedWith()); 334 $metaData = \OC::$server->getRequest()->getParam('talkMetaData') ?? ''; 335 $metaData = json_decode($metaData, true); 336 $metaData = is_array($metaData) ? $metaData : []; 337 338 if (isset($metaData['messageType']) && $metaData['messageType'] === 'voice-message') { 339 if ($share->getNode()->getMimeType() !== 'audio/mpeg' 340 && $share->getNode()->getMimeType() !== 'audio/wav') { 341 unset($metaData['messageType']); 342 } 343 } 344 345 $listener->sendSystemMessage($room, 'file_shared', ['share' => $share->getId(), 'metaData' => $metaData]); 346 }; 347 /** 348 * @psalm-suppress UndefinedClass 349 */ 350 $dispatcher->addListener('OCP\Share::postShare', $listener); 351 $dispatcher->addListener(RoomShareProvider::class . '::' . 'share_file_again', $listener); 352 } 353 354 public function handle(Event $event): void { 355 if ($event instanceof AttendeesAddedEvent) { 356 $this->attendeesAddedEvent($event); 357 } elseif ($event instanceof AttendeesRemovedEvent) { 358 $this->attendeesRemovedEvent($event); 359 } 360 } 361 362 protected function attendeesAddedEvent(AttendeesAddedEvent $event): void { 363 foreach ($event->getAttendees() as $attendee) { 364 if ($attendee->getActorType() === Attendee::ACTOR_GROUPS) { 365 $this->sendSystemMessage($event->getRoom(), 'group_added', ['group' => $attendee->getActorId()]); 366 } elseif ($attendee->getActorType() === Attendee::ACTOR_CIRCLES) { 367 $this->sendSystemMessage($event->getRoom(), 'circle_added', ['circle' => $attendee->getActorId()]); 368 } 369 } 370 } 371 372 protected function attendeesRemovedEvent(AttendeesRemovedEvent $event): void { 373 foreach ($event->getAttendees() as $attendee) { 374 if ($attendee->getActorType() === Attendee::ACTOR_GROUPS) { 375 $this->sendSystemMessage($event->getRoom(), 'group_removed', ['group' => $attendee->getActorId()]); 376 } elseif ($attendee->getActorType() === Attendee::ACTOR_CIRCLES) { 377 $this->sendSystemMessage($event->getRoom(), 'circle_removed', ['circle' => $attendee->getActorId()]); 378 } 379 } 380 } 381 382 protected function sendSystemMessage(Room $room, string $message, array $parameters = [], Participant $participant = null): void { 383 if ($participant instanceof Participant) { 384 $actorType = $participant->getAttendee()->getActorType(); 385 $actorId = $participant->getAttendee()->getActorId(); 386 } else { 387 $user = $this->userSession->getUser(); 388 if ($user instanceof IUser) { 389 $actorType = Attendee::ACTOR_USERS; 390 $actorId = $user->getUID(); 391 } elseif (\OC::$CLI || $this->session->exists('talk-overwrite-actor-cli')) { 392 $actorType = Attendee::ACTOR_GUESTS; 393 $actorId = 'cli'; 394 } elseif ($this->session->exists('talk-overwrite-actor')) { 395 $actorType = Attendee::ACTOR_USERS; 396 $actorId = $this->session->get('talk-overwrite-actor'); 397 } else { 398 $actorType = Attendee::ACTOR_GUESTS; 399 $sessionId = $this->talkSession->getSessionForRoom($room->getToken()); 400 $actorId = $sessionId ? sha1($sessionId) : 'failed-to-get-session'; 401 } 402 } 403 404 // Little hack to get the reference id from the share request into 405 // the system message left for the share in the chat. 406 $referenceId = $this->request->getParam('referenceId', null); 407 if ($referenceId !== null) { 408 $referenceId = (string) $referenceId; 409 } 410 411 $this->chatManager->addSystemMessage( 412 $room, $actorType, $actorId, 413 json_encode(['message' => $message, 'parameters' => $parameters]), 414 $this->timeFactory->getDateTime(), $message === 'file_shared', 415 $referenceId 416 ); 417 } 418 419 protected function getUserId(): ?string { 420 $user = $this->userSession->getUser(); 421 return $user instanceof IUser ? $user->getUID() : null; 422 } 423} 424