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