diff --git a/appinfo/info.xml b/appinfo/info.xml index b3884af95..dbc31e7f0 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.4.3 + 3.5.0.-dev.1 agpl Julien Veyssier Assistant diff --git a/lib/AppInfo/Application.php b/lib/AppInfo/Application.php index b1b2771f3..07296100e 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; @@ -47,6 +48,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 { @@ -100,6 +102,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/ChattyLLM/SessionMapper.php b/lib/Db/ChattyLLM/SessionMapper.php index 9006bcf63..660543c09 100644 --- a/lib/Db/ChattyLLM/SessionMapper.php +++ b/lib/Db/ChattyLLM/SessionMapper.php @@ -180,6 +180,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 000000000..acbc4b799 --- /dev/null +++ b/lib/Listener/UserDeletedListener.php @@ -0,0 +1,51 @@ + + */ +class UserDeletedListener implements IEventListener { + + public function __construct( + private ChatService $chatService, + private LoggerInterface $logger, + private SessionSummaryService $sessionSummaryService, + ) { + } + + public function handle(Event $event): void { + if (!($event instanceof UserDeletedEvent)) { + return; + } + + $userId = $event->getUid(); + + try { + $this->chatService->deleteAllUserChatData($userId); + } 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 000000000..60da26478 --- /dev/null +++ b/lib/Migration/Version030500Date20260715083046.php @@ -0,0 +1,93 @@ +getDistinctUserIds(); + $cleaned = 0; + + foreach ($userIds as $userId) { + if ($this->userManager->userExists($userId)) { + continue; + } + + 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 array + */ + 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(); + } + + $userIds = array_unique(array_merge($userIdsChat, $userIdsAssignments)); + return $userIds; + } +} diff --git a/lib/Service/ChatService.php b/lib/Service/ChatService.php new file mode 100644 index 000000000..97e325c5f --- /dev/null +++ b/lib/Service/ChatService.php @@ -0,0 +1,38 @@ +sessionMapper->getUserSessions($userId); + foreach ($sessions as $session) { + $this->messageMapper->deleteMessagesBySession($session->getId()); + } + $this->sessionMapper->deleteAllSessionsForUser($userId); + } catch (Exception|\RuntimeException $e) { + throw new InternalException(previous: $e); + } + } + +} diff --git a/lib/Service/InternalException.php b/lib/Service/InternalException.php new file mode 100644 index 000000000..28cd5c0d1 --- /dev/null +++ b/lib/Service/InternalException.php @@ -0,0 +1,11 @@ +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 {