Skip to content
Merged
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
1 change: 1 addition & 0 deletions appinfo/routes.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
['name' => 'assistantApi#displayUserFile', 'url' => '/api/{apiVersion}/file/{fileId}/display', 'verb' => 'GET', 'requirements' => $requirements],
['name' => 'assistantApi#getUserFileInfo', 'url' => '/api/{apiVersion}/file/{fileId}/info', 'verb' => 'GET', 'requirements' => $requirements],
['name' => 'assistantApi#shareOutputFile', 'url' => '/api/{apiVersion}/task/{ocpTaskId}/file/{fileId}/share', 'verb' => 'POST', 'requirements' => $requirements],
['name' => 'assistantApi#saveOutputFile', 'url' => '/api/{apiVersion}/task/{ocpTaskId}/file/{fileId}/save', 'verb' => 'POST', 'requirements' => $requirements],
['name' => 'assistantApi#getOutputFilePreview', 'url' => '/api/{apiVersion}/task/{ocpTaskId}/output-file/{fileId}/preview', 'verb' => 'GET', 'requirements' => $requirements],
['name' => 'assistantApi#getOutputFile', 'url' => '/api/{apiVersion}/task/{ocpTaskId}/output-file/{fileId}/download', 'verb' => 'GET', 'requirements' => $requirements],

Expand Down
4 changes: 4 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@
use OCA\Assistant\Listener\FreePrompt\FreePromptReferenceListener;
use OCA\Assistant\Listener\SpeechToText\SpeechToTextReferenceListener;
use OCA\Assistant\Listener\TaskFailedListener;
use OCA\Assistant\Listener\TaskOutputFileReferenceListener;
use OCA\Assistant\Listener\TaskSuccessfulListener;
use OCA\Assistant\Listener\Text2Image\Text2ImageReferenceListener;
use OCA\Assistant\Notification\Notifier;
use OCA\Assistant\Reference\FreePromptReferenceProvider;
use OCA\Assistant\Reference\SpeechToTextReferenceProvider;
use OCA\Assistant\Reference\TaskOutputFileReferenceProvider;
use OCA\Assistant\Reference\Text2ImageReferenceProvider;
use OCP\AppFramework\App;
use OCP\AppFramework\Bootstrap\IBootContext;
Expand Down Expand Up @@ -52,10 +54,12 @@ public function register(IRegistrationContext $context): void {
$context->registerReferenceProvider(Text2ImageReferenceProvider::class);
$context->registerReferenceProvider(FreePromptReferenceProvider::class);
$context->registerReferenceProvider(SpeechToTextReferenceProvider::class);
$context->registerReferenceProvider(TaskOutputFileReferenceProvider::class);

$context->registerEventListener(RenderReferenceEvent::class, Text2ImageReferenceListener::class);
$context->registerEventListener(RenderReferenceEvent::class, FreePromptReferenceListener::class);
$context->registerEventListener(RenderReferenceEvent::class, SpeechToTextReferenceListener::class);
$context->registerEventListener(RenderReferenceEvent::class, TaskOutputFileReferenceListener::class);

$context->registerEventListener(BeforeTemplateRenderedEvent::class, BeforeTemplateRenderedListener::class);

Expand Down
27 changes: 25 additions & 2 deletions lib/Controller/AssistantApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -234,13 +234,13 @@ public function getUserFileInfo(int $fileId): DataResponse {
/**
* Share an output file
*
* Share a file that was produced by a task
* Save and share a file that was produced by a task
*
* @param int $ocpTaskId The task ID
* @param int $fileId The file ID
* @return DataResponse<Http::STATUS_OK, array{shareToken: string}, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array{error: string}, array{}>
*
* 200: The file was shared
* 200: The file was saved and shared
* 404: The file was not found
*/
#[NoAdminRequired]
Expand All @@ -254,6 +254,29 @@ public function shareOutputFile(int $ocpTaskId, int $fileId): DataResponse {
}
}

/**
* Save an output file
*
* Save a file that was produced by a task
*
* @param int $ocpTaskId The task ID
* @param int $fileId The file ID
* @return DataResponse<Http::STATUS_OK, array{shareToken: string}, array{}>|DataResponse<Http::STATUS_NOT_FOUND, array{error: string}, array{}>
*
* 200: The file was saved
* 404: The file was not found
*/
#[NoAdminRequired]
public function saveOutputFile(int $ocpTaskId, int $fileId): DataResponse {
try {
$info = $this->assistantService->saveOutputFile($this->userId, $ocpTaskId, $fileId);
return new DataResponse($info);
} catch (\Exception $e) {
$this->logger->error('Failed to save assistant output file', ['exception' => $e]);
return new DataResponse(['error' => $e->getMessage()], Http::STATUS_NOT_FOUND);
}
}

/**
* Get task output file preview
*
Expand Down
3 changes: 3 additions & 0 deletions lib/Listener/BeforeTemplateRenderedListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ public function handle(Event $event): void {
$indexingComplete = $this->appConfig->getValueInt('context_chat', 'last_indexed_time', 0) !== 0;
$this->initialStateService->provideInitialState('contextChatIndexingComplete', $indexingComplete);
}
if (class_exists(\OCA\Viewer\Event\LoadViewer::class)) {
$this->eventDispatcher->dispatchTyped(new \OCA\Viewer\Event\LoadViewer());
}
Util::addScript(Application::APP_ID, Application::APP_ID . '-main');
}
}
31 changes: 31 additions & 0 deletions lib/Listener/TaskOutputFileReferenceListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

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

namespace OCA\Assistant\Listener;

use OCA\Assistant\AppInfo\Application;
use OCP\Collaboration\Reference\RenderReferenceEvent;
use OCP\EventDispatcher\Event;
use OCP\EventDispatcher\IEventListener;
use OCP\Util;

/**
* @implements IEventListener<RenderReferenceEvent>
*/
class TaskOutputFileReferenceListener implements IEventListener {
public function __construct(
) {
}

public function handle(Event $event): void {
if (!$event instanceof RenderReferenceEvent) {
return;
}

Util::addScript(Application::APP_ID, Application::APP_ID . '-taskOutputFileReference');
}
}
137 changes: 137 additions & 0 deletions lib/Reference/TaskOutputFileReferenceProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<?php

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

namespace OCA\Assistant\Reference;

use OCA\Assistant\AppInfo\Application;
use OCP\Collaboration\Reference\IReference;
use OCP\Collaboration\Reference\IReferenceManager;
use OCP\Collaboration\Reference\IReferenceProvider;
use OCP\Collaboration\Reference\LinkReferenceProvider;
use OCP\Collaboration\Reference\Reference;
use OCP\IURLGenerator;
use OCP\TaskProcessing\IManager as TaskProcessingManager;

class TaskOutputFileReferenceProvider implements IReferenceProvider {

private const RICH_OBJECT_TYPE = Application::APP_ID . '_task-output-file';

public function __construct(
private IReferenceManager $referenceManager,
private LinkReferenceProvider $linkReferenceProvider,
private IURLGenerator $urlGenerator,
private TaskProcessingManager $taskProcessingManager,
private ?string $userId,
) {
}

/**
* @inheritDoc
*/
public function matchReference(string $referenceText): bool {
return $this->getLinkInfo($referenceText) !== null;
}

/**
* @inheritDoc
*/
public function resolveReference(string $referenceText): ?IReference {
if ($this->matchReference($referenceText)) {
$linkInfo = $this->getLinkInfo($referenceText);
if ($linkInfo !== null) {
$taskId = $linkInfo['taskId'];
$task = $this->taskProcessingManager->getTask($taskId);
if ($task->getUserId() === null || $task->getUserId() !== $this->userId) {
return null;
}

$linkInfo['taskTypeId'] = $task->getTaskTypeId();
$linkInfo['taskTypeName'] = $this->taskProcessingManager->getAvailableTaskTypes()[$task->getTaskTypeId()]['name'] ?? null;
$reference = new Reference($referenceText);
$reference->setRichObject(
self::RICH_OBJECT_TYPE,
$linkInfo,
);
return $reference;
}
// fallback to opengraph
return $this->linkReferenceProvider->resolveReference($referenceText);
}

return null;
}

/**
* @param string $url
* @return array|null
*/
private function getLinkinfo(string $url): ?array {
// assistant download link
// https://nextcloud.local/ocs/v2.php/apps/assistant/api/v1/task/42/output-file/398/download

$start = $this->urlGenerator->linkToOCSRouteAbsolute(Application::APP_ID . '.assistantApi.getOutputFile', [
'apiVersion' => 'v1',
'ocpTaskId' => 123,
'fileId' => 123,
]);
$start = str_replace('/task/123/output-file/123/download', '/task/', $start);
if (str_starts_with($url, $start)) {
preg_match('/\/task\/(\d+)\/output-file\/(\d+)\/download$/i', $url, $matches);
if (count($matches) > 2) {
return [
'taskId' => (int)$matches[1],
'fileId' => (int)$matches[2],
];
}
}

// task processing download links
// https://nextcloud.local/ocs/v2.php/taskprocessing/tasks/42/file/398

$start = $this->urlGenerator->linkToOCSRouteAbsolute('core.taskProcessingApi.getFileContents', [
'taskId' => 123,
'fileId' => 123,
]);
$start = str_replace('/tasks/123/file/123', '/tasks/', $start);
if (str_starts_with($url, $start)) {
preg_match('/\/tasks\/(\d+)\/file\/(\d+)$/i', $url, $matches);
if (count($matches) > 2) {
return [
'taskId' => (int)$matches[1],
'fileId' => (int)$matches[2],
];
}
}

return null;
}

/**
* We use the userId here because when connecting/disconnecting from the GitHub account,
* we want to invalidate all the user cache and this is only possible with the cache prefix
* @inheritDoc
*/
public function getCachePrefix(string $referenceId): string {
return $this->userId ?? '';
}

/**
* We don't use the userId here but rather a reference unique id
* @inheritDoc
*/
public function getCacheKey(string $referenceId): ?string {
return $referenceId;
}

/**
* @param string $userId
* @return void
*/
public function invalidateUserCache(string $userId): void {
$this->referenceManager->invalidateCache($userId);
}
}
39 changes: 32 additions & 7 deletions lib/Service/AssistantService.php
Original file line number Diff line number Diff line change
Expand Up @@ -391,7 +391,7 @@ public function getTaskOutputFile(string $userId, int $ocpTaskId, int $fileId):
* @param string $userId
* @param int $ocpTaskId
* @param int $fileId
* @return string
* @return File
* @throws Exception
* @throws InvalidPathException
* @throws LockedException
Expand All @@ -402,34 +402,59 @@ public function getTaskOutputFile(string $userId, int $ocpTaskId, int $fileId):
* @throws TaskProcessingException
* @throws \OCP\Files\NotFoundException
*/
public function shareOutputFile(string $userId, int $ocpTaskId, int $fileId): string {
private function saveFile(string $userId, int $ocpTaskId, int $fileId): File {
$taskOutputFile = $this->getTaskOutputFile($userId, $ocpTaskId, $fileId);
$assistantDataFolder = $this->getAssistantDataFolder($userId);
$targetFileName = $this->getTargetFileName($taskOutputFile);
if ($assistantDataFolder->nodeExists($targetFileName)) {
$existingTarget = $assistantDataFolder->get($targetFileName);
if ($existingTarget instanceof File) {
if ($existingTarget->getSize() === $taskOutputFile->getSize()) {
$fileCopy = $existingTarget;
return $existingTarget;
} else {
$fileCopy = $assistantDataFolder->newFile($targetFileName, $taskOutputFile->fopen('rb'));
return $assistantDataFolder->newFile($targetFileName, $taskOutputFile->fopen('rb'));
}
} else {
throw new Exception('Impossible to copy output file, a directory with this name already exists', Http::STATUS_UNAUTHORIZED);
}
} else {
$fileCopy = $assistantDataFolder->newFile($targetFileName, $taskOutputFile->fopen('rb'));
return $assistantDataFolder->newFile($targetFileName, $taskOutputFile->fopen('rb'));
}
}

/**
* @param string $userId
* @param int $ocpTaskId
* @param int $fileId
* @return string
* @throws Exception
* @throws InvalidPathException
* @throws LockedException
* @throws NoUserException
* @throws NotFoundException
* @throws NotPermittedException
* @throws PreConditionNotMetException
* @throws TaskProcessingException
* @throws \OCP\Files\NotFoundException
*/
public function shareOutputFile(string $userId, int $ocpTaskId, int $fileId): string {
$fileCopy = $this->saveFile($userId, $ocpTaskId, $fileId);
$share = $this->shareManager->newShare();
$share->setNode($fileCopy);
$share->setPermissions(Constants::PERMISSION_READ);
$share->setShareType(IShare::TYPE_LINK);
$share->setSharedBy($userId);
$share->setLabel('Assistant share');
$share = $this->shareManager->createShare($share);
$shareToken = $share->getToken();
return $share->getToken();
}

return $shareToken;
public function saveOutputFile(string $userId, int $ocpTaskId, int $fileId): array {
$fileCopy = $this->saveFile($userId, $ocpTaskId, $fileId);
return [
'fileId' => $fileCopy->getId(),
'path' => preg_replace('/^files\//', '/', $fileCopy->getInternalPath()),
];
}

/**
Expand Down
Loading
Loading