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 {