Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
]]> </description>
<version>3.4.3</version>
<version>3.5.0.-dev.1</version>
<licence>agpl</licence>
<author>Julien Veyssier</author>
<namespace>Assistant</namespace>
Expand Down
4 changes: 4 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {

Expand Down Expand Up @@ -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);
}
Expand Down
11 changes: 11 additions & 0 deletions lib/Db/ChattyLLM/SessionMapper.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
51 changes: 51 additions & 0 deletions lib/Listener/UserDeletedListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Assistant\Listener;

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;
use Psr\Log\LoggerInterface;

/**
* @template-implements IEventListener<UserDeletedEvent>
*/
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]);
}
}
}
93 changes: 93 additions & 0 deletions lib/Migration/Version030500Date20260715083046.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Assistant\Migration;

use Closure;
use OCA\Assistant\Service\ChatService;
use OCA\Assistant\Service\InternalException;
use OCA\Assistant\Service\SessionSummaryService;
use OCP\DB\ISchemaWrapper;
use OCP\IDBConnection;
use OCP\IUserManager;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
use Override;
use Psr\Log\LoggerInterface;

class Version030500Date20260715083046 extends SimpleMigrationStep {

public function __construct(
private IDBConnection $db,
private IUserManager $userManager,
private ChatService $chatService,
private SessionSummaryService $sessionSummaryService,
private LoggerInterface $logger,
) {
}

/**
* Clean up chat and assignment data left behind by users deleted before UserDeletedListener existed.
*
* @param IOutput $output
* @param Closure(): ISchemaWrapper $schemaClosure
* @param array $options
*/
#[Override]
public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void {
$userIds = $this->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<int, mixed>
*/
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;
}
}
38 changes: 38 additions & 0 deletions lib/Service/ChatService.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Assistant\Service;

use OCA\Assistant\Db\ChattyLLM\MessageMapper;
use OCA\Assistant\Db\ChattyLLM\SessionMapper;
use OCP\DB\Exception;

class ChatService {
public const EMPTY_CONVERSATION_TOKEN = '{}';

public function __construct(
private readonly SessionMapper $sessionMapper,
private readonly MessageMapper $messageMapper,
) {
}

/**
* @throws InternalException
*/
public function deleteAllUserChatData(string $userId): void {
try {
$sessions = $this->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);
}
}

}
11 changes: 11 additions & 0 deletions lib/Service/InternalException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

/**
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\Assistant\Service;

class InternalException extends \Exception {
}
9 changes: 9 additions & 0 deletions lib/Service/SessionSummaryService.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,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 {
Expand Down
Loading