From f11783e59197908813bbb8d56ee2ea954bc82682 Mon Sep 17 00:00:00 2001 From: Anna Larch Date: Thu, 14 Nov 2024 20:55:34 +0100 Subject: [PATCH] feat(rooms): add option to automatically lock public and group rooms if they are inactive Signed-off-by: Anna Larch --- appinfo/info.xml | 3 +- docs/settings.md | 2 + lib/BackgroundJob/LockInactiveRooms.php | 56 +++++++ lib/Config.php | 11 ++ lib/Manager.php | 24 +++ lib/Service/RoomService.php | 7 + .../BackgroundJob/LockInactiveRoomsTest.php | 153 ++++++++++++++++++ 7 files changed, 255 insertions(+), 1 deletion(-) create mode 100644 lib/BackgroundJob/LockInactiveRooms.php create mode 100644 tests/php/BackgroundJob/LockInactiveRoomsTest.php diff --git a/appinfo/info.xml b/appinfo/info.xml index e88217fe344..5ae2f875bf8 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -18,7 +18,7 @@ * 🌉 **Sync with other chat solutions** With [Matterbridge](https://github.com/42wim/matterbridge/) being integrated in Talk, you can easily sync a lot of other chat solutions to Nextcloud Talk and vice-versa. ]]> - 21.0.0-dev.2 + 21.0.0-dev.3 agpl Anna Larch @@ -66,6 +66,7 @@ OCA\Talk\BackgroundJob\CheckTurnCertificate OCA\Talk\BackgroundJob\ExpireChatMessages OCA\Talk\BackgroundJob\ExpireSignalingMessage + OCA\Talk\BackgroundJob\LockInactiveRooms OCA\Talk\BackgroundJob\MaximumCallDuration OCA\Talk\BackgroundJob\Reminder OCA\Talk\BackgroundJob\RemoveEmptyRooms diff --git a/docs/settings.md b/docs/settings.md index dc2cfe836c1..b828fe2cf33 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -113,3 +113,5 @@ Legend: | `conversations_files` | string
`1` or `0` | `1` | No | 🖌️ | Whether the files app integration is enabled allowing to start conversations in the right sidebar | | `conversations_files_public_shares` | string
`1` or `0` | `1` | No | 🖌️ | Whether the public share integration is enabled allowing to start conversations in the right sidebar on the public share page (Requires `conversations_files` also to be enabled) | | `enable_matterbridge` | string
`1` or `0` | `0` | No | 🖌️ | Whether the Matterbridge integration is enabled and can be configured | +| `inactivity_lock_after_days` | int | `0` | No | | A duration (in days) after which rooms are locked. Calculated from the last activity in the room. | +| `inactivity_enable_lobby` | string
`1` or `0` | `0` | No | | Additionally enable the lobby for inactive rooms so they can only be read by moderators. | diff --git a/lib/BackgroundJob/LockInactiveRooms.php b/lib/BackgroundJob/LockInactiveRooms.php new file mode 100644 index 00000000000..ae30df4c306 --- /dev/null +++ b/lib/BackgroundJob/LockInactiveRooms.php @@ -0,0 +1,56 @@ +setInterval(60 * 60 * 24); + $this->setTimeSensitivity(IJob::TIME_SENSITIVE); + } + + /** + * @inheritDoc + */ + public function run($argument): void { + $interval = $this->appConfig->getInactiveLockTime(); + $forceLobby = $this->appConfig->enableLobbyOnLockedRooms(); + if ($interval === 0) { + return; + } + $timestamp = $this->time->getTime() - $interval * 60 * 60 * 24; + $time = $this->time->getDateTime('@' . $timestamp); + $rooms = $this->roomService->getInactiveRooms($time); + array_map(function (Room $room) use ($forceLobby) { + $this->roomService->setReadOnly($room, Room::READ_ONLY); + $this->logger->debug("Locking room {$room->getId()} due to inactivity"); + if ($forceLobby) { + $this->roomService->setLobby($room, Webinary::LOBBY_NON_MODERATORS, $this->time->getDateTime()); + $this->logger->debug("Enabling lobby for room {$room->getId()}"); + } + }, $rooms); + } +} diff --git a/lib/Config.php b/lib/Config.php index 87f26c52cd5..ccf88382bc2 100644 --- a/lib/Config.php +++ b/lib/Config.php @@ -694,4 +694,15 @@ public function getBlurVirtualBackground(?string $userId): bool { } return false; } + + /** + * User setting falling back to admin defined app config + */ + public function getInactiveLockTime(): int { + return $this->appConfig->getAppValueInt('inactivity_lock_after_days'); + } + + public function enableLobbyOnLockedRooms(): bool { + return $this->appConfig->getAppValueBool('inactivity_enable_lobby'); + } } diff --git a/lib/Manager.php b/lib/Manager.php index a0b4c7fe111..28e311a6887 100644 --- a/lib/Manager.php +++ b/lib/Manager.php @@ -317,6 +317,30 @@ public function getRoomsLongerActiveSince(\DateTime $maxActiveSince): array { return $rooms; } + /** + * @return list + */ + public function getInactiveRooms(\DateTime $inactiveSince): array { + $query = $this->db->getQueryBuilder(); + $helper = new SelectHelper(); + $helper->selectRoomsTable($query); + $query->from('talk_rooms', 'r') + ->andWhere($query->expr()->lte('r.last_activity', $query->createNamedParameter($inactiveSince, IQueryBuilder::PARAM_DATETIME_MUTABLE))) + ->andWhere($query->expr()->neq('r.read_only', $query->createNamedParameter(Room::READ_ONLY, IQueryBuilder::PARAM_INT))) + ->andWhere($query->expr()->in('r.type', $query->createNamedParameter([Room::TYPE_PUBLIC, Room::TYPE_GROUP], IQueryBuilder::PARAM_INT_ARRAY))) + ->andWhere($query->expr()->emptyString('remoteServer')) + ->orderBy('r.id', 'ASC'); + $result = $query->executeQuery(); + + $rooms = []; + while ($row = $result->fetch()) { + $rooms[] = $this->createRoomObject($row); + } + $result->closeCursor(); + + return $rooms; + } + /** * @param string $userId * @param array $sessionIds A list of talk sessions to consider for loading (otherwise no session is loaded) diff --git a/lib/Service/RoomService.php b/lib/Service/RoomService.php index a813e906596..82e7e70ae7e 100644 --- a/lib/Service/RoomService.php +++ b/lib/Service/RoomService.php @@ -1212,4 +1212,11 @@ public function deleteRoom(Room $room): void { )); } } + + /** + * @return list + */ + public function getInactiveRooms(\DateTime $inactiveSince): array { + return $this->manager->getInactiveRooms($inactiveSince); + } } diff --git a/tests/php/BackgroundJob/LockInactiveRoomsTest.php b/tests/php/BackgroundJob/LockInactiveRoomsTest.php new file mode 100644 index 00000000000..4a3662af5ee --- /dev/null +++ b/tests/php/BackgroundJob/LockInactiveRoomsTest.php @@ -0,0 +1,153 @@ +timeFactory = $this->createMock(ITimeFactory::class); + $this->roomService = $this->createMock(RoomService::class); + $this->appConfig = $this->createMock(Config::class); + $this->logger = $this->createMock(LoggerInterface::class); + $this->job = new LockInactiveRooms($this->timeFactory, + $this->roomService, + $this->appConfig, + $this->logger + ); + } + + public function testNotEnabled(): void { + $this->appConfig->expects(self::once()) + ->method('getInactiveLockTime') + ->willReturn(0); + $this->appConfig->expects(self::once()) + ->method('enableLobbyOnLockedRooms') + ->willReturn(false); + $this->timeFactory->expects(self::never()) + ->method(self::anything()); + $this->roomService->expects(self::never()) + ->method(self::anything()); + $this->logger->expects(self::never()) + ->method(self::anything()); + + $this->job->run('t'); + } + + public function testNoRooms(): void { + $this->appConfig->expects(self::once()) + ->method('getInactiveLockTime') + ->willReturn(123); + $this->appConfig->expects(self::once()) + ->method('enableLobbyOnLockedRooms') + ->willReturn(false); + $this->timeFactory->expects(self::once()) + ->method('getTime'); + $this->timeFactory->expects(self::once()) + ->method('getDateTime'); + $this->roomService->expects(self::once()) + ->method('getInactiveRooms') + ->willReturn([]); + $this->roomService->expects(self::never()) + ->method('setReadOnly'); + $this->roomService->expects(self::never()) + ->method('setLobby'); + $this->logger->expects(self::never()) + ->method(self::anything()); + + $this->job->run('t'); + + } + + public function testLockRooms(): void { + $rooms = [ + $this->createConfiguredMock(Room::class, [ + 'getReadOnly' => 0, + 'getType' => Room::TYPE_PUBLIC, + ]), + $this->createConfiguredMock(Room::class, [ + 'getReadOnly' => 0, + 'getType' => Room::TYPE_GROUP, + ]), + ]; + + $this->appConfig->expects(self::once()) + ->method('getInactiveLockTime') + ->willReturn(123); + $this->appConfig->expects(self::once()) + ->method('enableLobbyOnLockedRooms') + ->willReturn(false); + $this->timeFactory->expects(self::once()) + ->method('getTime'); + $this->timeFactory->expects(self::once()) + ->method('getDateTime'); + $this->roomService->expects(self::once()) + ->method('getInactiveRooms') + ->willReturn($rooms); + $this->roomService->expects(self::exactly(2)) + ->method('setReadOnly'); + $this->roomService->expects(self::never()) + ->method('setLobby'); + $this->logger->expects(self::exactly(2)) + ->method('debug'); + + $this->job->run('t'); + + } + + public function testLockRoomsAndEnableLobby(): void { + $rooms = [ + $this->createConfiguredMock(Room::class, [ + 'getReadOnly' => 0, + 'getType' => Room::TYPE_PUBLIC, + ]), + $this->createConfiguredMock(Room::class, [ + 'getReadOnly' => 0, + 'getType' => Room::TYPE_GROUP, + ]), + ]; + + $this->appConfig->expects(self::once()) + ->method('getInactiveLockTime') + ->willReturn(123); + $this->appConfig->expects(self::once()) + ->method('enableLobbyOnLockedRooms') + ->willReturn(true); + $this->timeFactory->expects(self::once()) + ->method('getTime'); + $this->timeFactory->expects(self::any()) + ->method('getDateTime'); + $this->roomService->expects(self::once()) + ->method('getInactiveRooms') + ->willReturn($rooms); + $this->roomService->expects(self::exactly(2)) + ->method('setReadOnly'); + $this->roomService->expects(self::exactly(2)) + ->method('setLobby'); + $this->logger->expects(self::exactly(4)) + ->method('debug'); + + $this->job->run('t'); + } +}