From 8165fab7d6b1e37ea820fa4c51e7bb7bf3ac2a43 Mon Sep 17 00:00:00 2001 From: Jana Peper Date: Wed, 15 Jul 2026 13:34:59 +0200 Subject: [PATCH 1/2] feat: Delete chat messages and assignments when user is deleted and stop running assignments Signed-off-by: Jana Peper --- lib/AppInfo/Application.php | 4 +++ lib/Db/AssignmentMapper.php | 11 ++++++ lib/Db/ChattyLLM/SessionMapper.php | 24 +++++++++++++ lib/Listener/UserDeletedListener.php | 51 ++++++++++++++++++++++++++++ lib/Service/AssignmentsService.php | 20 +++++++++++ lib/Service/ChatService.php | 15 ++++++++ 6 files changed, 125 insertions(+) create mode 100644 lib/Listener/UserDeletedListener.php diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index baaac70a..c054d46d 100644 --- a/lib/AppInfo/Application.php +++ b/lib/AppInfo/Application.php @@ -23,6 +23,7 @@ use OCA\Assistant\Listener\TaskSuccessfulListener; use OCA\Assistant\Listener\Text2Image\Text2ImageReferenceListener; use OCA\Assistant\Listener\Text2Image\Text2StickerListener; +use OCA\Assistant\Listener\UserDeletedListener; use OCA\Assistant\Notification\Notifier; use OCA\Assistant\Reference\FreePromptReferenceProvider; use OCA\Assistant\Reference\SpeechToTextReferenceProvider; @@ -48,6 +49,7 @@ use OCP\TaskProcessing\Events\TaskFailedEvent; use OCP\TaskProcessing\Events\TaskSuccessfulEvent; use OCP\TaskProcessing\IManager; +use OCP\User\Events\UserDeletedEvent; class Application extends App implements IBootstrap { @@ -102,6 +104,8 @@ public function register(IRegistrationContext $context): void { $context->registerEventListener(AddContentSecurityPolicyEvent::class, CSPListener::class); + $context->registerEventListener(UserDeletedEvent::class, UserDeletedListener::class); + if (class_exists('OCP\\TaskProcessing\\TaskTypes\\AudioToAudioChat')) { $context->registerTaskProcessingProvider(AudioToAudioChatProvider::class); } diff --git a/lib/Db/AssignmentMapper.php b/lib/Db/AssignmentMapper.php index f4746254..a6f3024e 100644 --- a/lib/Db/AssignmentMapper.php +++ b/lib/Db/AssignmentMapper.php @@ -86,4 +86,15 @@ public function findDueAssignmentsForUser(string $userId): \Generator { yield $assignment; } } + + /** + * @throws \OCP\DB\Exception + */ + public function deleteAllForUser(string $userId): void { + $qb = $this->db->getQueryBuilder(); + $qb->delete($this->getTableName()) + ->where($qb->expr()->eq('user_id', $qb->createPositionalParameter($userId, IQueryBuilder::PARAM_STR))); + + $qb->executeStatement(); + } } diff --git a/lib/Db/ChattyLLM/SessionMapper.php b/lib/Db/ChattyLLM/SessionMapper.php index dfc6fe73..c8359173 100644 --- a/lib/Db/ChattyLLM/SessionMapper.php +++ b/lib/Db/ChattyLLM/SessionMapper.php @@ -82,6 +82,19 @@ public function getUserSessionForAssignment(string $userId, int $assignmentId): return $this->findEntity($qb); } + /** + * @return \Generator + * @throws \OCP\DB\Exception + */ + public function getAllUserSessions(string $userId): \Generator { + $qb = $this->db->getQueryBuilder(); + $qb->select(Session::$columns) + ->from($this->getTableName()) + ->where($qb->expr()->eq('user_id', $qb->createPositionalParameter($userId, IQueryBuilder::PARAM_STR))); + + yield from $this->yieldEntities($qb); + } + /** * @param string $userId * @param bool $isAssignment @@ -205,6 +218,17 @@ public function deleteSession(string $userId, int $sessionId) { $qb->executeStatement(); } + /** + * @throws \OCP\DB\Exception + */ + public function deleteAllSessionsForUser(string $userId): void { + $qb = $this->db->getQueryBuilder(); + $qb->delete($this->getTableName()) + ->where($qb->expr()->eq('user_id', $qb->createPositionalParameter($userId, IQueryBuilder::PARAM_STR))); + + $qb->executeStatement(); + } + public function updateSessionIsRemembered(?string $userId, int $sessionId, bool $is_remembered) { $session = $this->getUserSession($userId, $sessionId); $session->setIsRemembered($is_remembered); diff --git a/lib/Listener/UserDeletedListener.php b/lib/Listener/UserDeletedListener.php new file mode 100644 index 00000000..57c37555 --- /dev/null +++ b/lib/Listener/UserDeletedListener.php @@ -0,0 +1,51 @@ + + */ +class UserDeletedListener implements IEventListener { + + public function __construct( + private ChatService $chatService, + private AssignmentsService $assignmentsService, + private LoggerInterface $logger, + ) { + } + + public function handle(Event $event): void { + if (!($event instanceof UserDeletedEvent)) { + return; + } + + $userId = $event->getUid(); + + try { + $this->assignmentsService->deleteAllForUser($userId); + } catch (InternalException $e) { + $this->logger->error('Error while deleting assignments for user ' . $userId, ['exception' => $e]); + } + + try { + $this->chatService->deleteAllUserChatData($userId); + } catch (InternalException $e) { + $this->logger->error('Error while deleting chat data for user ' . $userId, ['exception' => $e]); + } + } +} diff --git a/lib/Service/AssignmentsService.php b/lib/Service/AssignmentsService.php index 82095488..75178fe2 100644 --- a/lib/Service/AssignmentsService.php +++ b/lib/Service/AssignmentsService.php @@ -21,6 +21,7 @@ use OCP\DB\Exception; use OCP\IDateTimeZone; use OCP\IL10N; +use OCP\IUserManager; use Psr\Log\LoggerInterface; class AssignmentsService { @@ -33,6 +34,7 @@ public function __construct( private IJobList $jobList, private IL10N $l10n, private IDateTimeZone $dateTimeZone, + private IUserManager $userManager, ) { } @@ -84,6 +86,20 @@ public function createAssignment(?string $userId, string $title, string $prompt, return $assignment; } + /** + * @throws InternalException + */ + public function deleteAllForUser(string $userId): void { + try { + $this->assignmentMapper->deleteAllForUser($userId); + } catch (Exception $e) { + throw new InternalException(previous: $e); + } + if ($this->jobList->has(RunAssignmentsJob::class, ['userId' => $userId])) { + $this->jobList->remove(RunAssignmentsJob::class, ['userId' => $userId]); + } + } + /** * @throws InternalException|UnauthorizedException */ @@ -91,6 +107,10 @@ public function runDueAssignmentsForUser(?string $userId): void { if ($userId === null) { throw new UnauthorizedException(); } + if ($this->userManager->get($userId) === null) { + $this->deleteAllForUser($userId); + return; + } try { foreach ($this->assignmentMapper->findDueAssignmentsForUser($userId) as $assignment) { if ($assignment === null) { diff --git a/lib/Service/ChatService.php b/lib/Service/ChatService.php index bda24454..0a5986a5 100644 --- a/lib/Service/ChatService.php +++ b/lib/Service/ChatService.php @@ -152,6 +152,21 @@ public function deleteSession(?string $userId, int $sessionId): void { } } + /** + * @throws InternalException + */ + public function deleteAllUserChatData(string $userId): void { + try { + $sessions = $this->sessionMapper->getAllUserSessions($userId); + foreach ($sessions as $session) { + $this->messageMapper->deleteMessagesBySession($session->getId()); + } + $this->sessionMapper->deleteAllSessionsForUser($userId); + } catch (Exception|\RuntimeException $e) { + throw new InternalException(previous: $e); + } + } + /** * @return list * @throws InternalException From 72456875ef57fece4b01ac4b836cd4664ef1318d Mon Sep 17 00:00:00 2001 From: Lukas Schaefer Date: Wed, 15 Jul 2026 09:03:27 -0400 Subject: [PATCH 2/2] Add cleanup job and also delete summary generation jobs Signed-off-by: Lukas Schaefer --- appinfo/info.xml | 2 +- lib/Listener/UserDeletedListener.php | 8 ++ .../Version030500Date20260715083046.php | 110 ++++++++++++++++++ lib/Service/SessionSummaryService.php | 9 ++ 4 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 lib/Migration/Version030500Date20260715083046.php diff --git a/appinfo/info.xml b/appinfo/info.xml index 0dcc790f..f9a4992d 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -62,7 +62,7 @@ Known providers: More details on how to set this up in the [admin docs](https://docs.nextcloud.com/server/latest/admin_manual/ai/index.html) ]]> - 3.5.0-dev.3 + 3.5.0-dev.4 agpl Julien Veyssier Assistant diff --git a/lib/Listener/UserDeletedListener.php b/lib/Listener/UserDeletedListener.php index 57c37555..ca997a5d 100644 --- a/lib/Listener/UserDeletedListener.php +++ b/lib/Listener/UserDeletedListener.php @@ -12,6 +12,7 @@ use OCA\Assistant\Service\AssignmentsService; use OCA\Assistant\Service\ChatService; use OCA\Assistant\Service\InternalException; +use OCA\Assistant\Service\SessionSummaryService; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; use OCP\User\Events\UserDeletedEvent; @@ -26,6 +27,7 @@ public function __construct( private ChatService $chatService, private AssignmentsService $assignmentsService, private LoggerInterface $logger, + private SessionSummaryService $sessionSummaryService, ) { } @@ -47,5 +49,11 @@ public function handle(Event $event): void { } catch (InternalException $e) { $this->logger->error('Error while deleting chat data for user ' . $userId, ['exception' => $e]); } + + try { + $this->sessionSummaryService->deleteSummaryJobsForUser($userId); + } catch (InternalException $e) { + $this->logger->error('Error while deleting summary jobs for user ' . $userId, ['exception' => $e]); + } } } diff --git a/lib/Migration/Version030500Date20260715083046.php b/lib/Migration/Version030500Date20260715083046.php new file mode 100644 index 00000000..0777011e --- /dev/null +++ b/lib/Migration/Version030500Date20260715083046.php @@ -0,0 +1,110 @@ +getDistinctUserIds(); + $cleaned = 0; + + foreach ($userIds as $userId) { + if ($this->userManager->userExists($userId)) { + continue; + } + + try { + $this->assignmentsService->deleteAllForUser($userId); + } catch (InternalException $e) { + $this->logger->error('Error while deleting assignments for deleted user ' . $userId, ['exception' => $e]); + } + + try { + $this->chatService->deleteAllUserChatData($userId); + } catch (InternalException $e) { + $this->logger->error('Error while deleting chat data for deleted user ' . $userId, ['exception' => $e]); + } + + try { + $this->sessionSummaryService->deleteSummaryJobsForUser($userId); + } catch (InternalException $e) { + $this->logger->error('Error while deleting summary jobs for deleted user ' . $userId, ['exception' => $e]); + } + + $cleaned++; + } + + if ($cleaned > 0) { + $output->info('Cleaned up assistant data for ' . $cleaned . ' deleted user(s)'); + } + } + + /** + * @return list + */ + private function getDistinctUserIds(): array { + $userIds = []; + + $userIdsChat = []; + $userIdsAssignments = []; + + if ($this->db->tableExists('assistant_chat_sns')) { + $qb = $this->db->getQueryBuilder(); + $qb->selectDistinct('user_id') + ->from('assistant_chat_sns'); + $result = $qb->executeQuery(); + $userIdsChat = $result->fetchFirstColumn(); + $result->closeCursor(); + } + + if ($this->db->tableExists('assistant_assignments')) { + $qb = $this->db->getQueryBuilder(); + $qb->selectDistinct('user_id') + ->from('assistant_assignments'); + $result = $qb->executeQuery(); + $userIdsAssignments = $result->fetchFirstColumn(); + $result->closeCursor(); + } + + $userIds = array_unique(array_merge($userIdsChat, $userIdsAssignments)); + return $userIds; + } +} diff --git a/lib/Service/SessionSummaryService.php b/lib/Service/SessionSummaryService.php index 51b5b174..ffe0e533 100644 --- a/lib/Service/SessionSummaryService.php +++ b/lib/Service/SessionSummaryService.php @@ -42,6 +42,15 @@ public function __construct( ) { } + public function deleteSummaryJobsForUser(string $userId): void { + if ($this->jobList->has(GenerateNewChatSummaries::class, ['userId' => $userId])) { + $this->jobList->remove(GenerateNewChatSummaries::class, ['userId' => $userId]); + } + if ($this->jobList->has(RegenerateOutdatedChatSummariesJob::class, ['userId' => $userId])) { + $this->jobList->remove(RegenerateOutdatedChatSummariesJob::class, ['userId' => $userId]); + } + } + private function generateSummaries(array $sessions): void { foreach ($sessions as $session) { try {