Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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