Skip to content

Commit 64a630a

Browse files
authored
Merge pull request #606 from nextcloud/backport/604/stable34
[stable34] feat: Delete chat messages and assignments when user is deleted
2 parents 5631c11 + 93066f1 commit 64a630a

9 files changed

Lines changed: 219 additions & 2 deletions

File tree

.github/workflows/phpunit.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ jobs:
3737
matrix:
3838
php-versions: ['8.2']
3939
databases: ['sqlite']
40-
server-versions: ['stable33', 'master']
40+
server-versions: ['stable33', 'stable34']
4141

4242
name: php${{ matrix.php-versions }}-${{ matrix.databases }}-${{ matrix.server-versions }}
4343

appinfo/info.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ Known providers:
6262
6363
More details on how to set this up in the [admin docs](https://docs.nextcloud.com/server/latest/admin_manual/ai/index.html)
6464
]]> </description>
65-
<version>3.4.3</version>
65+
<version>3.5.0.-dev.1</version>
6666
<licence>agpl</licence>
6767
<author>Julien Veyssier</author>
6868
<namespace>Assistant</namespace>

lib/AppInfo/Application.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use OCA\Assistant\Listener\TaskSuccessfulListener;
2424
use OCA\Assistant\Listener\Text2Image\Text2ImageReferenceListener;
2525
use OCA\Assistant\Listener\Text2Image\Text2StickerListener;
26+
use OCA\Assistant\Listener\UserDeletedListener;
2627
use OCA\Assistant\Notification\Notifier;
2728
use OCA\Assistant\Reference\FreePromptReferenceProvider;
2829
use OCA\Assistant\Reference\SpeechToTextReferenceProvider;
@@ -47,6 +48,7 @@
4748
use OCP\TaskProcessing\Events\TaskFailedEvent;
4849
use OCP\TaskProcessing\Events\TaskSuccessfulEvent;
4950
use OCP\TaskProcessing\IManager;
51+
use OCP\User\Events\UserDeletedEvent;
5052

5153
class Application extends App implements IBootstrap {
5254

@@ -100,6 +102,8 @@ public function register(IRegistrationContext $context): void {
100102

101103
$context->registerEventListener(AddContentSecurityPolicyEvent::class, CSPListener::class);
102104

105+
$context->registerEventListener(UserDeletedEvent::class, UserDeletedListener::class);
106+
103107
if (class_exists('OCP\\TaskProcessing\\TaskTypes\\AudioToAudioChat')) {
104108
$context->registerTaskProcessingProvider(AudioToAudioChatProvider::class);
105109
}

lib/Db/ChattyLLM/SessionMapper.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,17 @@ public function deleteSession(string $userId, int $sessionId) {
180180
$qb->executeStatement();
181181
}
182182

183+
/**
184+
* @throws \OCP\DB\Exception
185+
*/
186+
public function deleteAllSessionsForUser(string $userId): void {
187+
$qb = $this->db->getQueryBuilder();
188+
$qb->delete($this->getTableName())
189+
->where($qb->expr()->eq('user_id', $qb->createPositionalParameter($userId, IQueryBuilder::PARAM_STR)));
190+
191+
$qb->executeStatement();
192+
}
193+
183194
public function updateSessionIsRemembered(?string $userId, int $sessionId, bool $is_remembered) {
184195
$session = $this->getUserSession($userId, $sessionId);
185196
$session->setIsRemembered($is_remembered);
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\Assistant\Listener;
11+
12+
use OCA\Assistant\Service\ChatService;
13+
use OCA\Assistant\Service\InternalException;
14+
use OCA\Assistant\Service\SessionSummaryService;
15+
use OCP\EventDispatcher\Event;
16+
use OCP\EventDispatcher\IEventListener;
17+
use OCP\User\Events\UserDeletedEvent;
18+
use Psr\Log\LoggerInterface;
19+
20+
/**
21+
* @template-implements IEventListener<UserDeletedEvent>
22+
*/
23+
class UserDeletedListener implements IEventListener {
24+
25+
public function __construct(
26+
private ChatService $chatService,
27+
private LoggerInterface $logger,
28+
private SessionSummaryService $sessionSummaryService,
29+
) {
30+
}
31+
32+
public function handle(Event $event): void {
33+
if (!($event instanceof UserDeletedEvent)) {
34+
return;
35+
}
36+
37+
$userId = $event->getUid();
38+
39+
try {
40+
$this->chatService->deleteAllUserChatData($userId);
41+
} catch (InternalException $e) {
42+
$this->logger->error('Error while deleting chat data for user ' . $userId, ['exception' => $e]);
43+
}
44+
45+
try {
46+
$this->sessionSummaryService->deleteSummaryJobsForUser($userId);
47+
} catch (InternalException $e) {
48+
$this->logger->error('Error while deleting summary jobs for user ' . $userId, ['exception' => $e]);
49+
}
50+
}
51+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\Assistant\Migration;
11+
12+
use Closure;
13+
use OCA\Assistant\Service\ChatService;
14+
use OCA\Assistant\Service\InternalException;
15+
use OCA\Assistant\Service\SessionSummaryService;
16+
use OCP\DB\ISchemaWrapper;
17+
use OCP\IDBConnection;
18+
use OCP\IUserManager;
19+
use OCP\Migration\IOutput;
20+
use OCP\Migration\SimpleMigrationStep;
21+
use Override;
22+
use Psr\Log\LoggerInterface;
23+
24+
class Version030500Date20260715083046 extends SimpleMigrationStep {
25+
26+
public function __construct(
27+
private IDBConnection $db,
28+
private IUserManager $userManager,
29+
private ChatService $chatService,
30+
private SessionSummaryService $sessionSummaryService,
31+
private LoggerInterface $logger,
32+
) {
33+
}
34+
35+
/**
36+
* Clean up chat and assignment data left behind by users deleted before UserDeletedListener existed.
37+
*
38+
* @param IOutput $output
39+
* @param Closure(): ISchemaWrapper $schemaClosure
40+
* @param array $options
41+
*/
42+
#[Override]
43+
public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
44+
$userIds = $this->getDistinctUserIds();
45+
$cleaned = 0;
46+
47+
foreach ($userIds as $userId) {
48+
if ($this->userManager->userExists($userId)) {
49+
continue;
50+
}
51+
52+
try {
53+
$this->chatService->deleteAllUserChatData($userId);
54+
} catch (InternalException $e) {
55+
$this->logger->error('Error while deleting chat data for deleted user ' . $userId, ['exception' => $e]);
56+
}
57+
58+
try {
59+
$this->sessionSummaryService->deleteSummaryJobsForUser($userId);
60+
} catch (InternalException $e) {
61+
$this->logger->error('Error while deleting summary jobs for deleted user ' . $userId, ['exception' => $e]);
62+
}
63+
64+
$cleaned++;
65+
}
66+
67+
if ($cleaned > 0) {
68+
$output->info('Cleaned up assistant data for ' . $cleaned . ' deleted user(s)');
69+
}
70+
}
71+
72+
/**
73+
* @return array<int, mixed>
74+
*/
75+
private function getDistinctUserIds(): array {
76+
$userIds = [];
77+
78+
$userIdsChat = [];
79+
$userIdsAssignments = [];
80+
81+
if ($this->db->tableExists('assistant_chat_sns')) {
82+
$qb = $this->db->getQueryBuilder();
83+
$qb->selectDistinct('user_id')
84+
->from('assistant_chat_sns');
85+
$result = $qb->executeQuery();
86+
$userIdsChat = $result->fetchFirstColumn();
87+
$result->closeCursor();
88+
}
89+
90+
$userIds = array_unique(array_merge($userIdsChat, $userIdsAssignments));
91+
return $userIds;
92+
}
93+
}

lib/Service/ChatService.php

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
/**
4+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
namespace OCA\Assistant\Service;
9+
10+
use OCA\Assistant\Db\ChattyLLM\MessageMapper;
11+
use OCA\Assistant\Db\ChattyLLM\SessionMapper;
12+
use OCP\DB\Exception;
13+
14+
class ChatService {
15+
public const EMPTY_CONVERSATION_TOKEN = '{}';
16+
17+
public function __construct(
18+
private readonly SessionMapper $sessionMapper,
19+
private readonly MessageMapper $messageMapper,
20+
) {
21+
}
22+
23+
/**
24+
* @throws InternalException
25+
*/
26+
public function deleteAllUserChatData(string $userId): void {
27+
try {
28+
$sessions = $this->sessionMapper->getUserSessions($userId);
29+
foreach ($sessions as $session) {
30+
$this->messageMapper->deleteMessagesBySession($session->getId());
31+
}
32+
$this->sessionMapper->deleteAllSessionsForUser($userId);
33+
} catch (Exception|\RuntimeException $e) {
34+
throw new InternalException(previous: $e);
35+
}
36+
}
37+
38+
}

lib/Service/InternalException.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
/**
4+
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
namespace OCA\Assistant\Service;
9+
10+
class InternalException extends \Exception {
11+
}

lib/Service/SessionSummaryService.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,15 @@ public function __construct(
4141
) {
4242
}
4343

44+
public function deleteSummaryJobsForUser(string $userId): void {
45+
if ($this->jobList->has(GenerateNewChatSummaries::class, ['userId' => $userId])) {
46+
$this->jobList->remove(GenerateNewChatSummaries::class, ['userId' => $userId]);
47+
}
48+
if ($this->jobList->has(RegenerateOutdatedChatSummariesJob::class, ['userId' => $userId])) {
49+
$this->jobList->remove(RegenerateOutdatedChatSummariesJob::class, ['userId' => $userId]);
50+
}
51+
}
52+
4453
private function generateSummaries(array $sessions): void {
4554
foreach ($sessions as $session) {
4655
try {

0 commit comments

Comments
 (0)