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/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..ca997a5d
--- /dev/null
+++ b/lib/Listener/UserDeletedListener.php
@@ -0,0 +1,59 @@
+
+ */
+class UserDeletedListener implements IEventListener {
+
+ public function __construct(
+ private ChatService $chatService,
+ private AssignmentsService $assignmentsService,
+ private LoggerInterface $logger,
+ private SessionSummaryService $sessionSummaryService,
+ ) {
+ }
+
+ 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]);
+ }
+
+ 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/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
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 {