Completed
Push — master ( d84d6c...b12b38 )
by Marcel
29:09
created
lib/private/TaskProcessing/Manager.php 1 patch
Indentation   +1589 added lines, -1589 removed lines patch added patch discarded remove patch
@@ -70,1593 +70,1593 @@
 block discarded – undo
70 70
 
71 71
 class Manager implements IManager {
72 72
 
73
-	public const LEGACY_PREFIX_TEXTPROCESSING = 'legacy:TextProcessing:';
74
-	public const LEGACY_PREFIX_TEXTTOIMAGE = 'legacy:TextToImage:';
75
-	public const LEGACY_PREFIX_SPEECHTOTEXT = 'legacy:SpeechToText:';
76
-
77
-	public const LAZY_CONFIG_KEYS = [
78
-		'ai.taskprocessing_type_preferences',
79
-		'ai.taskprocessing_provider_preferences',
80
-	];
81
-
82
-	public const MAX_TASK_AGE_SECONDS = 60 * 60 * 24 * 31 * 6; // 6 months
83
-
84
-	private const TASK_TYPES_CACHE_KEY = 'available_task_types_v3';
85
-	private const TASK_TYPE_IDS_CACHE_KEY = 'available_task_type_ids';
86
-
87
-	/** @var list<IProvider>|null */
88
-	private ?array $providers = null;
89
-
90
-	/**
91
-	 * @var array<array-key,array{name: string, description: string, inputShape: ShapeDescriptor[], inputShapeEnumValues: ShapeEnumValue[][], inputShapeDefaults: array<array-key, numeric|string>, optionalInputShape: ShapeDescriptor[], optionalInputShapeEnumValues: ShapeEnumValue[][], optionalInputShapeDefaults: array<array-key, numeric|string>, outputShape: ShapeDescriptor[], outputShapeEnumValues: ShapeEnumValue[][], optionalOutputShape: ShapeDescriptor[], optionalOutputShapeEnumValues: ShapeEnumValue[][]}>
92
-	 */
93
-	private ?array $availableTaskTypes = null;
94
-
95
-	/** @var list<string>|null */
96
-	private ?array $availableTaskTypeIds = null;
97
-
98
-	private IAppData $appData;
99
-	private ?array $preferences = null;
100
-	private ?array $providersById = null;
101
-
102
-	/** @var ITaskType[]|null */
103
-	private ?array $taskTypes = null;
104
-	private ICache $distributedCache;
105
-
106
-	private ?GetTaskProcessingProvidersEvent $eventResult = null;
107
-
108
-	public function __construct(
109
-		private IAppConfig $appConfig,
110
-		private Coordinator $coordinator,
111
-		private IServerContainer $serverContainer,
112
-		private LoggerInterface $logger,
113
-		private TaskMapper $taskMapper,
114
-		private IJobList $jobList,
115
-		private IEventDispatcher $dispatcher,
116
-		IAppDataFactory $appDataFactory,
117
-		private IRootFolder $rootFolder,
118
-		private \OCP\TextToImage\IManager $textToImageManager,
119
-		private IUserMountCache $userMountCache,
120
-		private IClientService $clientService,
121
-		private IAppManager $appManager,
122
-		private IUserManager $userManager,
123
-		private IUserSession $userSession,
124
-		ICacheFactory $cacheFactory,
125
-		private IFactory $l10nFactory,
126
-	) {
127
-		$this->appData = $appDataFactory->get('core');
128
-		$this->distributedCache = $cacheFactory->createDistributed('task_processing::');
129
-	}
130
-
131
-
132
-	/**
133
-	 * This is almost a copy of textProcessingManager->getProviders
134
-	 * to avoid a dependency cycle between TextProcessingManager and TaskProcessingManager
135
-	 */
136
-	private function _getRawTextProcessingProviders(): array {
137
-		$context = $this->coordinator->getRegistrationContext();
138
-		if ($context === null) {
139
-			return [];
140
-		}
141
-
142
-		$providers = [];
143
-
144
-		foreach ($context->getTextProcessingProviders() as $providerServiceRegistration) {
145
-			$class = $providerServiceRegistration->getService();
146
-			try {
147
-				$providers[$class] = $this->serverContainer->get($class);
148
-			} catch (\Throwable $e) {
149
-				$this->logger->error('Failed to load Text processing provider ' . $class, [
150
-					'exception' => $e,
151
-				]);
152
-			}
153
-		}
154
-
155
-		return $providers;
156
-	}
157
-
158
-	private function _getTextProcessingProviders(): array {
159
-		$oldProviders = $this->_getRawTextProcessingProviders();
160
-		$newProviders = [];
161
-		foreach ($oldProviders as $oldProvider) {
162
-			$provider = new class($oldProvider) implements IProvider, ISynchronousProvider {
163
-				private \OCP\TextProcessing\IProvider $provider;
164
-
165
-				public function __construct(\OCP\TextProcessing\IProvider $provider) {
166
-					$this->provider = $provider;
167
-				}
168
-
169
-				public function getId(): string {
170
-					if ($this->provider instanceof \OCP\TextProcessing\IProviderWithId) {
171
-						return $this->provider->getId();
172
-					}
173
-					return Manager::LEGACY_PREFIX_TEXTPROCESSING . $this->provider::class;
174
-				}
175
-
176
-				public function getName(): string {
177
-					return $this->provider->getName();
178
-				}
179
-
180
-				public function getTaskTypeId(): string {
181
-					return match ($this->provider->getTaskType()) {
182
-						\OCP\TextProcessing\FreePromptTaskType::class => TextToText::ID,
183
-						\OCP\TextProcessing\HeadlineTaskType::class => TextToTextHeadline::ID,
184
-						\OCP\TextProcessing\TopicsTaskType::class => TextToTextTopics::ID,
185
-						\OCP\TextProcessing\SummaryTaskType::class => TextToTextSummary::ID,
186
-						default => Manager::LEGACY_PREFIX_TEXTPROCESSING . $this->provider->getTaskType(),
187
-					};
188
-				}
189
-
190
-				public function getExpectedRuntime(): int {
191
-					if ($this->provider instanceof \OCP\TextProcessing\IProviderWithExpectedRuntime) {
192
-						return $this->provider->getExpectedRuntime();
193
-					}
194
-					return 60;
195
-				}
196
-
197
-				public function getOptionalInputShape(): array {
198
-					return [];
199
-				}
200
-
201
-				public function getOptionalOutputShape(): array {
202
-					return [];
203
-				}
204
-
205
-				public function process(?string $userId, array $input, callable $reportProgress): array {
206
-					if ($this->provider instanceof \OCP\TextProcessing\IProviderWithUserId) {
207
-						$this->provider->setUserId($userId);
208
-					}
209
-					try {
210
-						return ['output' => $this->provider->process($input['input'])];
211
-					} catch (\RuntimeException $e) {
212
-						throw new ProcessingException($e->getMessage(), 0, $e);
213
-					}
214
-				}
215
-
216
-				public function getInputShapeEnumValues(): array {
217
-					return [];
218
-				}
219
-
220
-				public function getInputShapeDefaults(): array {
221
-					return [];
222
-				}
223
-
224
-				public function getOptionalInputShapeEnumValues(): array {
225
-					return [];
226
-				}
227
-
228
-				public function getOptionalInputShapeDefaults(): array {
229
-					return [];
230
-				}
231
-
232
-				public function getOutputShapeEnumValues(): array {
233
-					return [];
234
-				}
235
-
236
-				public function getOptionalOutputShapeEnumValues(): array {
237
-					return [];
238
-				}
239
-			};
240
-			$newProviders[$provider->getId()] = $provider;
241
-		}
242
-
243
-		return $newProviders;
244
-	}
245
-
246
-	/**
247
-	 * @return ITaskType[]
248
-	 */
249
-	private function _getTextProcessingTaskTypes(): array {
250
-		$oldProviders = $this->_getRawTextProcessingProviders();
251
-		$newTaskTypes = [];
252
-		foreach ($oldProviders as $oldProvider) {
253
-			// These are already implemented in the TaskProcessing realm
254
-			if (in_array($oldProvider->getTaskType(), [
255
-				\OCP\TextProcessing\FreePromptTaskType::class,
256
-				\OCP\TextProcessing\HeadlineTaskType::class,
257
-				\OCP\TextProcessing\TopicsTaskType::class,
258
-				\OCP\TextProcessing\SummaryTaskType::class
259
-			], true)) {
260
-				continue;
261
-			}
262
-			$taskType = new class($oldProvider->getTaskType()) implements ITaskType {
263
-				private string $oldTaskTypeClass;
264
-				private \OCP\TextProcessing\ITaskType $oldTaskType;
265
-				private IL10N $l;
266
-
267
-				public function __construct(string $oldTaskTypeClass) {
268
-					$this->oldTaskTypeClass = $oldTaskTypeClass;
269
-					$this->oldTaskType = \OCP\Server::get($oldTaskTypeClass);
270
-					$this->l = \OCP\Server::get(IFactory::class)->get('core');
271
-				}
272
-
273
-				public function getId(): string {
274
-					return Manager::LEGACY_PREFIX_TEXTPROCESSING . $this->oldTaskTypeClass;
275
-				}
276
-
277
-				public function getName(): string {
278
-					return $this->oldTaskType->getName();
279
-				}
280
-
281
-				public function getDescription(): string {
282
-					return $this->oldTaskType->getDescription();
283
-				}
284
-
285
-				public function getInputShape(): array {
286
-					return ['input' => new ShapeDescriptor($this->l->t('Input text'), $this->l->t('The input text'), EShapeType::Text)];
287
-				}
288
-
289
-				public function getOutputShape(): array {
290
-					return ['output' => new ShapeDescriptor($this->l->t('Input text'), $this->l->t('The input text'), EShapeType::Text)];
291
-				}
292
-			};
293
-			$newTaskTypes[$taskType->getId()] = $taskType;
294
-		}
295
-
296
-		return $newTaskTypes;
297
-	}
298
-
299
-	/**
300
-	 * @return IProvider[]
301
-	 */
302
-	private function _getTextToImageProviders(): array {
303
-		$oldProviders = $this->textToImageManager->getProviders();
304
-		$newProviders = [];
305
-		foreach ($oldProviders as $oldProvider) {
306
-			$newProvider = new class($oldProvider, $this->appData) implements IProvider, ISynchronousProvider {
307
-				private \OCP\TextToImage\IProvider $provider;
308
-				private IAppData $appData;
309
-
310
-				public function __construct(\OCP\TextToImage\IProvider $provider, IAppData $appData) {
311
-					$this->provider = $provider;
312
-					$this->appData = $appData;
313
-				}
314
-
315
-				public function getId(): string {
316
-					return Manager::LEGACY_PREFIX_TEXTTOIMAGE . $this->provider->getId();
317
-				}
318
-
319
-				public function getName(): string {
320
-					return $this->provider->getName();
321
-				}
322
-
323
-				public function getTaskTypeId(): string {
324
-					return TextToImage::ID;
325
-				}
326
-
327
-				public function getExpectedRuntime(): int {
328
-					return $this->provider->getExpectedRuntime();
329
-				}
330
-
331
-				public function getOptionalInputShape(): array {
332
-					return [];
333
-				}
334
-
335
-				public function getOptionalOutputShape(): array {
336
-					return [];
337
-				}
338
-
339
-				public function process(?string $userId, array $input, callable $reportProgress): array {
340
-					try {
341
-						$folder = $this->appData->getFolder('text2image');
342
-					} catch (\OCP\Files\NotFoundException) {
343
-						$folder = $this->appData->newFolder('text2image');
344
-					}
345
-					$resources = [];
346
-					$files = [];
347
-					for ($i = 0; $i < $input['numberOfImages']; $i++) {
348
-						$file = $folder->newFile(time() . '-' . rand(1, 100000) . '-' . $i);
349
-						$files[] = $file;
350
-						$resource = $file->write();
351
-						if ($resource !== false && $resource !== true && is_resource($resource)) {
352
-							$resources[] = $resource;
353
-						} else {
354
-							throw new ProcessingException('Text2Image generation using provider "' . $this->getName() . '" failed: Couldn\'t open file to write.');
355
-						}
356
-					}
357
-					if ($this->provider instanceof \OCP\TextToImage\IProviderWithUserId) {
358
-						$this->provider->setUserId($userId);
359
-					}
360
-					try {
361
-						$this->provider->generate($input['input'], $resources);
362
-					} catch (\RuntimeException $e) {
363
-						throw new ProcessingException($e->getMessage(), 0, $e);
364
-					}
365
-					for ($i = 0; $i < $input['numberOfImages']; $i++) {
366
-						if (is_resource($resources[$i])) {
367
-							// If $resource hasn't been closed yet, we'll do that here
368
-							fclose($resources[$i]);
369
-						}
370
-					}
371
-					return ['images' => array_map(fn (ISimpleFile $file) => $file->getContent(), $files)];
372
-				}
373
-
374
-				public function getInputShapeEnumValues(): array {
375
-					return [];
376
-				}
377
-
378
-				public function getInputShapeDefaults(): array {
379
-					return [];
380
-				}
381
-
382
-				public function getOptionalInputShapeEnumValues(): array {
383
-					return [];
384
-				}
385
-
386
-				public function getOptionalInputShapeDefaults(): array {
387
-					return [];
388
-				}
389
-
390
-				public function getOutputShapeEnumValues(): array {
391
-					return [];
392
-				}
393
-
394
-				public function getOptionalOutputShapeEnumValues(): array {
395
-					return [];
396
-				}
397
-			};
398
-			$newProviders[$newProvider->getId()] = $newProvider;
399
-		}
400
-
401
-		return $newProviders;
402
-	}
403
-
404
-	/**
405
-	 * This is almost a copy of SpeechToTextManager->getProviders
406
-	 * to avoid a dependency cycle between SpeechToTextManager and TaskProcessingManager
407
-	 */
408
-	private function _getRawSpeechToTextProviders(): array {
409
-		$context = $this->coordinator->getRegistrationContext();
410
-		if ($context === null) {
411
-			return [];
412
-		}
413
-		$providers = [];
414
-		foreach ($context->getSpeechToTextProviders() as $providerServiceRegistration) {
415
-			$class = $providerServiceRegistration->getService();
416
-			try {
417
-				$providers[$class] = $this->serverContainer->get($class);
418
-			} catch (NotFoundExceptionInterface|ContainerExceptionInterface|\Throwable $e) {
419
-				$this->logger->error('Failed to load SpeechToText provider ' . $class, [
420
-					'exception' => $e,
421
-				]);
422
-			}
423
-		}
424
-
425
-		return $providers;
426
-	}
427
-
428
-	/**
429
-	 * @return IProvider[]
430
-	 */
431
-	private function _getSpeechToTextProviders(): array {
432
-		$oldProviders = $this->_getRawSpeechToTextProviders();
433
-		$newProviders = [];
434
-		foreach ($oldProviders as $oldProvider) {
435
-			$newProvider = new class($oldProvider, $this->rootFolder, $this->appData) implements IProvider, ISynchronousProvider {
436
-				private ISpeechToTextProvider $provider;
437
-				private IAppData $appData;
438
-
439
-				private IRootFolder $rootFolder;
440
-
441
-				public function __construct(ISpeechToTextProvider $provider, IRootFolder $rootFolder, IAppData $appData) {
442
-					$this->provider = $provider;
443
-					$this->rootFolder = $rootFolder;
444
-					$this->appData = $appData;
445
-				}
446
-
447
-				public function getId(): string {
448
-					if ($this->provider instanceof ISpeechToTextProviderWithId) {
449
-						return Manager::LEGACY_PREFIX_SPEECHTOTEXT . $this->provider->getId();
450
-					}
451
-					return Manager::LEGACY_PREFIX_SPEECHTOTEXT . $this->provider::class;
452
-				}
453
-
454
-				public function getName(): string {
455
-					return $this->provider->getName();
456
-				}
457
-
458
-				public function getTaskTypeId(): string {
459
-					return AudioToText::ID;
460
-				}
461
-
462
-				public function getExpectedRuntime(): int {
463
-					return 60;
464
-				}
465
-
466
-				public function getOptionalInputShape(): array {
467
-					return [];
468
-				}
469
-
470
-				public function getOptionalOutputShape(): array {
471
-					return [];
472
-				}
473
-
474
-				public function process(?string $userId, array $input, callable $reportProgress): array {
475
-					if ($this->provider instanceof \OCP\SpeechToText\ISpeechToTextProviderWithUserId) {
476
-						$this->provider->setUserId($userId);
477
-					}
478
-					try {
479
-						$result = $this->provider->transcribeFile($input['input']);
480
-					} catch (\RuntimeException $e) {
481
-						throw new ProcessingException($e->getMessage(), 0, $e);
482
-					}
483
-					return ['output' => $result];
484
-				}
485
-
486
-				public function getInputShapeEnumValues(): array {
487
-					return [];
488
-				}
489
-
490
-				public function getInputShapeDefaults(): array {
491
-					return [];
492
-				}
493
-
494
-				public function getOptionalInputShapeEnumValues(): array {
495
-					return [];
496
-				}
497
-
498
-				public function getOptionalInputShapeDefaults(): array {
499
-					return [];
500
-				}
501
-
502
-				public function getOutputShapeEnumValues(): array {
503
-					return [];
504
-				}
505
-
506
-				public function getOptionalOutputShapeEnumValues(): array {
507
-					return [];
508
-				}
509
-			};
510
-			$newProviders[$newProvider->getId()] = $newProvider;
511
-		}
512
-
513
-		return $newProviders;
514
-	}
515
-
516
-	/**
517
-	 * Dispatches the event to collect external providers and task types.
518
-	 * Caches the result within the request.
519
-	 */
520
-	private function dispatchGetProvidersEvent(): GetTaskProcessingProvidersEvent {
521
-		if ($this->eventResult !== null) {
522
-			return $this->eventResult;
523
-		}
524
-
525
-		$this->eventResult = new GetTaskProcessingProvidersEvent();
526
-		$this->dispatcher->dispatchTyped($this->eventResult);
527
-		return $this->eventResult ;
528
-	}
529
-
530
-	/**
531
-	 * @return IProvider[]
532
-	 */
533
-	private function _getProviders(): array {
534
-		$context = $this->coordinator->getRegistrationContext();
535
-
536
-		if ($context === null) {
537
-			return [];
538
-		}
539
-
540
-		$providers = [];
541
-
542
-		foreach ($context->getTaskProcessingProviders() as $providerServiceRegistration) {
543
-			$class = $providerServiceRegistration->getService();
544
-			try {
545
-				/** @var IProvider $provider */
546
-				$provider = $this->serverContainer->get($class);
547
-				if (isset($providers[$provider->getId()])) {
548
-					$this->logger->warning('Task processing provider ' . $class . ' is using ID ' . $provider->getId() . ' which is already used by ' . $providers[$provider->getId()]::class);
549
-				}
550
-				$providers[$provider->getId()] = $provider;
551
-			} catch (\Throwable $e) {
552
-				$this->logger->error('Failed to load task processing provider ' . $class, [
553
-					'exception' => $e,
554
-				]);
555
-			}
556
-		}
557
-
558
-		$event = $this->dispatchGetProvidersEvent();
559
-		$externalProviders = $event->getProviders();
560
-		foreach ($externalProviders as $provider) {
561
-			if (!isset($providers[$provider->getId()])) {
562
-				$providers[$provider->getId()] = $provider;
563
-			} else {
564
-				$this->logger->info('Skipping external task processing provider with ID ' . $provider->getId() . ' because a local provider with the same ID already exists.');
565
-			}
566
-		}
567
-
568
-		$providers += $this->_getTextProcessingProviders() + $this->_getTextToImageProviders() + $this->_getSpeechToTextProviders();
569
-
570
-		return $providers;
571
-	}
572
-
573
-	/**
574
-	 * @return ITaskType[]
575
-	 */
576
-	private function _getTaskTypes(): array {
577
-		$context = $this->coordinator->getRegistrationContext();
578
-
579
-		if ($context === null) {
580
-			return [];
581
-		}
582
-
583
-		if ($this->taskTypes !== null) {
584
-			return $this->taskTypes;
585
-		}
586
-
587
-		// Default task types
588
-		$taskTypes = [
589
-			\OCP\TaskProcessing\TaskTypes\TextToText::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToText::class),
590
-			\OCP\TaskProcessing\TaskTypes\TextToTextTopics::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextTopics::class),
591
-			\OCP\TaskProcessing\TaskTypes\TextToTextHeadline::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextHeadline::class),
592
-			\OCP\TaskProcessing\TaskTypes\TextToTextSummary::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextSummary::class),
593
-			\OCP\TaskProcessing\TaskTypes\TextToTextFormalization::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextFormalization::class),
594
-			\OCP\TaskProcessing\TaskTypes\TextToTextSimplification::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextSimplification::class),
595
-			\OCP\TaskProcessing\TaskTypes\TextToTextChat::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextChat::class),
596
-			\OCP\TaskProcessing\TaskTypes\TextToTextTranslate::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextTranslate::class),
597
-			\OCP\TaskProcessing\TaskTypes\TextToTextReformulation::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextReformulation::class),
598
-			\OCP\TaskProcessing\TaskTypes\TextToImage::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToImage::class),
599
-			\OCP\TaskProcessing\TaskTypes\AudioToText::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\AudioToText::class),
600
-			\OCP\TaskProcessing\TaskTypes\ContextWrite::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\ContextWrite::class),
601
-			\OCP\TaskProcessing\TaskTypes\GenerateEmoji::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\GenerateEmoji::class),
602
-			\OCP\TaskProcessing\TaskTypes\TextToTextChangeTone::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextChangeTone::class),
603
-			\OCP\TaskProcessing\TaskTypes\TextToTextChatWithTools::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextChatWithTools::class),
604
-			\OCP\TaskProcessing\TaskTypes\ContextAgentInteraction::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\ContextAgentInteraction::class),
605
-			\OCP\TaskProcessing\TaskTypes\TextToTextProofread::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextProofread::class),
606
-			\OCP\TaskProcessing\TaskTypes\TextToSpeech::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToSpeech::class),
607
-			\OCP\TaskProcessing\TaskTypes\AudioToAudioChat::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\AudioToAudioChat::class),
608
-			\OCP\TaskProcessing\TaskTypes\ContextAgentAudioInteraction::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\ContextAgentAudioInteraction::class),
609
-			\OCP\TaskProcessing\TaskTypes\AnalyzeImages::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\AnalyzeImages::class),
610
-		];
611
-
612
-		foreach ($context->getTaskProcessingTaskTypes() as $providerServiceRegistration) {
613
-			$class = $providerServiceRegistration->getService();
614
-			try {
615
-				/** @var ITaskType $provider */
616
-				$taskType = $this->serverContainer->get($class);
617
-				if (isset($taskTypes[$taskType->getId()])) {
618
-					$this->logger->warning('Task processing task type ' . $class . ' is using ID ' . $taskType->getId() . ' which is already used by ' . $taskTypes[$taskType->getId()]::class);
619
-				}
620
-				$taskTypes[$taskType->getId()] = $taskType;
621
-			} catch (\Throwable $e) {
622
-				$this->logger->error('Failed to load task processing task type ' . $class, [
623
-					'exception' => $e,
624
-				]);
625
-			}
626
-		}
627
-
628
-		$event = $this->dispatchGetProvidersEvent();
629
-		$externalTaskTypes = $event->getTaskTypes();
630
-		foreach ($externalTaskTypes as $taskType) {
631
-			if (isset($taskTypes[$taskType->getId()])) {
632
-				$this->logger->warning('External task processing task type is using ID ' . $taskType->getId() . ' which is already used by a locally registered task type (' . get_class($taskTypes[$taskType->getId()]) . ')');
633
-			}
634
-			$taskTypes[$taskType->getId()] = $taskType;
635
-		}
636
-
637
-		$taskTypes += $this->_getTextProcessingTaskTypes();
638
-
639
-		$this->taskTypes = $taskTypes;
640
-		return $this->taskTypes;
641
-	}
642
-
643
-	/**
644
-	 * @return array
645
-	 */
646
-	private function _getTaskTypeSettings(): array {
647
-		try {
648
-			$json = $this->appConfig->getValueString('core', 'ai.taskprocessing_type_preferences', '', lazy: true);
649
-			if ($json === '') {
650
-				return [];
651
-			}
652
-			return json_decode($json, true, flags: JSON_THROW_ON_ERROR);
653
-		} catch (\JsonException $e) {
654
-			$this->logger->error('Failed to get settings. JSON Error in ai.taskprocessing_type_preferences', ['exception' => $e]);
655
-			$taskTypeSettings = [];
656
-			$taskTypes = $this->_getTaskTypes();
657
-			foreach ($taskTypes as $taskType) {
658
-				$taskTypeSettings[$taskType->getId()] = false;
659
-			};
660
-
661
-			return $taskTypeSettings;
662
-		}
663
-
664
-	}
665
-
666
-	/**
667
-	 * @param ShapeDescriptor[] $spec
668
-	 * @param array<array-key, string|numeric> $defaults
669
-	 * @param array<array-key, ShapeEnumValue[]> $enumValues
670
-	 * @param array $io
671
-	 * @param bool $optional
672
-	 * @return void
673
-	 * @throws ValidationException
674
-	 */
675
-	private static function validateInput(array $spec, array $defaults, array $enumValues, array $io, bool $optional = false): void {
676
-		foreach ($spec as $key => $descriptor) {
677
-			$type = $descriptor->getShapeType();
678
-			if (!isset($io[$key])) {
679
-				if ($optional) {
680
-					continue;
681
-				}
682
-				if (isset($defaults[$key])) {
683
-					if (EShapeType::getScalarType($type) !== $type) {
684
-						throw new ValidationException('Provider tried to set a default value for a non-scalar slot');
685
-					}
686
-					if (EShapeType::isFileType($type)) {
687
-						throw new ValidationException('Provider tried to set a default value for a slot that is not text or number');
688
-					}
689
-					$type->validateInput($defaults[$key]);
690
-					continue;
691
-				}
692
-				throw new ValidationException('Missing key: "' . $key . '"');
693
-			}
694
-			try {
695
-				$type->validateInput($io[$key]);
696
-				if ($type === EShapeType::Enum) {
697
-					if (!isset($enumValues[$key])) {
698
-						throw new ValidationException('Provider did not provide enum values for an enum slot: "' . $key . '"');
699
-					}
700
-					$type->validateEnum($io[$key], $enumValues[$key]);
701
-				}
702
-			} catch (ValidationException $e) {
703
-				throw new ValidationException('Failed to validate input key "' . $key . '": ' . $e->getMessage());
704
-			}
705
-		}
706
-	}
707
-
708
-	/**
709
-	 * Takes task input data and replaces fileIds with File objects
710
-	 *
711
-	 * @param array<array-key, list<numeric|string>|numeric|string> $input
712
-	 * @param array<array-key, numeric|string> ...$defaultSpecs the specs
713
-	 * @return array<array-key, list<numeric|string>|numeric|string>
714
-	 */
715
-	public function fillInputDefaults(array $input, ...$defaultSpecs): array {
716
-		$spec = array_reduce($defaultSpecs, fn ($carry, $spec) => array_merge($carry, $spec), []);
717
-		return array_merge($spec, $input);
718
-	}
719
-
720
-	/**
721
-	 * @param ShapeDescriptor[] $spec
722
-	 * @param array<array-key, ShapeEnumValue[]> $enumValues
723
-	 * @param array $io
724
-	 * @param bool $optional
725
-	 * @return void
726
-	 * @throws ValidationException
727
-	 */
728
-	private static function validateOutputWithFileIds(array $spec, array $enumValues, array $io, bool $optional = false): void {
729
-		foreach ($spec as $key => $descriptor) {
730
-			$type = $descriptor->getShapeType();
731
-			if (!isset($io[$key])) {
732
-				if ($optional) {
733
-					continue;
734
-				}
735
-				throw new ValidationException('Missing key: "' . $key . '"');
736
-			}
737
-			try {
738
-				$type->validateOutputWithFileIds($io[$key]);
739
-				if (isset($enumValues[$key])) {
740
-					$type->validateEnum($io[$key], $enumValues[$key]);
741
-				}
742
-			} catch (ValidationException $e) {
743
-				throw new ValidationException('Failed to validate output key "' . $key . '": ' . $e->getMessage());
744
-			}
745
-		}
746
-	}
747
-
748
-	/**
749
-	 * @param ShapeDescriptor[] $spec
750
-	 * @param array<array-key, ShapeEnumValue[]> $enumValues
751
-	 * @param array $io
752
-	 * @param bool $optional
753
-	 * @return void
754
-	 * @throws ValidationException
755
-	 */
756
-	private static function validateOutputWithFileData(array $spec, array $enumValues, array $io, bool $optional = false): void {
757
-		foreach ($spec as $key => $descriptor) {
758
-			$type = $descriptor->getShapeType();
759
-			if (!isset($io[$key])) {
760
-				if ($optional) {
761
-					continue;
762
-				}
763
-				throw new ValidationException('Missing key: "' . $key . '"');
764
-			}
765
-			try {
766
-				$type->validateOutputWithFileData($io[$key]);
767
-				if (isset($enumValues[$key])) {
768
-					$type->validateEnum($io[$key], $enumValues[$key]);
769
-				}
770
-			} catch (ValidationException $e) {
771
-				throw new ValidationException('Failed to validate output key "' . $key . '": ' . $e->getMessage());
772
-			}
773
-		}
774
-	}
775
-
776
-	/**
777
-	 * @param array<array-key, T> $array The array to filter
778
-	 * @param ShapeDescriptor[] ...$specs the specs that define which keys to keep
779
-	 * @return array<array-key, T>
780
-	 * @psalm-template T
781
-	 */
782
-	private function removeSuperfluousArrayKeys(array $array, ...$specs): array {
783
-		$keys = array_unique(array_reduce($specs, fn ($carry, $spec) => array_merge($carry, array_keys($spec)), []));
784
-		$keys = array_filter($keys, fn ($key) => array_key_exists($key, $array));
785
-		$values = array_map(fn (string $key) => $array[$key], $keys);
786
-		return array_combine($keys, $values);
787
-	}
788
-
789
-	public function hasProviders(): bool {
790
-		return count($this->getProviders()) !== 0;
791
-	}
792
-
793
-	public function getProviders(): array {
794
-		if ($this->providers === null) {
795
-			$this->providers = $this->_getProviders();
796
-		}
797
-
798
-		return $this->providers;
799
-	}
800
-
801
-	public function getPreferredProvider(string $taskTypeId) {
802
-		try {
803
-			if ($this->preferences === null) {
804
-				$this->preferences = $this->distributedCache->get('ai.taskprocessing_provider_preferences');
805
-				if ($this->preferences === null) {
806
-					$this->preferences = json_decode(
807
-						$this->appConfig->getValueString('core', 'ai.taskprocessing_provider_preferences', 'null', lazy: true),
808
-						associative: true,
809
-						flags: JSON_THROW_ON_ERROR,
810
-					);
811
-					$this->distributedCache->set('ai.taskprocessing_provider_preferences', $this->preferences, 60 * 3);
812
-				}
813
-			}
814
-
815
-			$providers = $this->getProviders();
816
-			if (isset($this->preferences[$taskTypeId])) {
817
-				$providersById = $this->providersById ?? array_reduce($providers, static function (array $carry, IProvider $provider) {
818
-					$carry[$provider->getId()] = $provider;
819
-					return $carry;
820
-				}, []);
821
-				$this->providersById = $providersById;
822
-				if (isset($providersById[$this->preferences[$taskTypeId]])) {
823
-					return $providersById[$this->preferences[$taskTypeId]];
824
-				}
825
-			}
826
-			// By default, use the first available provider
827
-			foreach ($providers as $provider) {
828
-				if ($provider->getTaskTypeId() === $taskTypeId) {
829
-					return $provider;
830
-				}
831
-			}
832
-		} catch (\JsonException $e) {
833
-			$this->logger->warning('Failed to parse provider preferences while getting preferred provider for task type ' . $taskTypeId, ['exception' => $e]);
834
-		}
835
-		throw new \OCP\TaskProcessing\Exception\Exception('No matching provider found');
836
-	}
837
-
838
-	public function getAvailableTaskTypes(bool $showDisabled = false, ?string $userId = null): array {
839
-		// We cache by language, because some task type fields are translated
840
-		$cacheKey = self::TASK_TYPES_CACHE_KEY . ':' . $this->l10nFactory->findLanguage();
841
-
842
-		// userId will be obtained from the session if left to null
843
-		if (!$this->checkGuestAccess($userId)) {
844
-			return [];
845
-		}
846
-		if ($this->availableTaskTypes === null) {
847
-			$cachedValue = $this->distributedCache->get($cacheKey);
848
-			if ($cachedValue !== null) {
849
-				$this->availableTaskTypes = unserialize($cachedValue);
850
-			}
851
-		}
852
-		// Either we have no cache or showDisabled is turned on, which we don't want to cache, ever.
853
-		if ($this->availableTaskTypes === null || $showDisabled) {
854
-			$taskTypes = $this->_getTaskTypes();
855
-			$taskTypeSettings = $this->_getTaskTypeSettings();
856
-
857
-			$availableTaskTypes = [];
858
-			foreach ($taskTypes as $taskType) {
859
-				if ((!$showDisabled) && isset($taskTypeSettings[$taskType->getId()]) && !$taskTypeSettings[$taskType->getId()]) {
860
-					continue;
861
-				}
862
-				try {
863
-					$provider = $this->getPreferredProvider($taskType->getId());
864
-				} catch (\OCP\TaskProcessing\Exception\Exception $e) {
865
-					continue;
866
-				}
867
-				try {
868
-					$availableTaskTypes[$provider->getTaskTypeId()] = [
869
-						'name' => $taskType->getName(),
870
-						'description' => $taskType->getDescription(),
871
-						'optionalInputShape' => $provider->getOptionalInputShape(),
872
-						'inputShapeEnumValues' => $provider->getInputShapeEnumValues(),
873
-						'inputShapeDefaults' => $provider->getInputShapeDefaults(),
874
-						'inputShape' => $taskType->getInputShape(),
875
-						'optionalInputShapeEnumValues' => $provider->getOptionalInputShapeEnumValues(),
876
-						'optionalInputShapeDefaults' => $provider->getOptionalInputShapeDefaults(),
877
-						'outputShape' => $taskType->getOutputShape(),
878
-						'outputShapeEnumValues' => $provider->getOutputShapeEnumValues(),
879
-						'optionalOutputShape' => $provider->getOptionalOutputShape(),
880
-						'optionalOutputShapeEnumValues' => $provider->getOptionalOutputShapeEnumValues(),
881
-					];
882
-				} catch (\Throwable $e) {
883
-					$this->logger->error('Failed to set up TaskProcessing provider ' . $provider::class, ['exception' => $e]);
884
-				}
885
-			}
886
-
887
-			if ($showDisabled) {
888
-				// Do not cache showDisabled, ever.
889
-				return $availableTaskTypes;
890
-			}
891
-
892
-			$this->availableTaskTypes = $availableTaskTypes;
893
-			$this->distributedCache->set($cacheKey, serialize($this->availableTaskTypes), 60);
894
-		}
895
-
896
-
897
-		return $this->availableTaskTypes;
898
-	}
899
-	public function getAvailableTaskTypeIds(bool $showDisabled = false, ?string $userId = null): array {
900
-		// userId will be obtained from the session if left to null
901
-		if (!$this->checkGuestAccess($userId)) {
902
-			return [];
903
-		}
904
-		if ($this->availableTaskTypeIds === null) {
905
-			$cachedValue = $this->distributedCache->get(self::TASK_TYPE_IDS_CACHE_KEY);
906
-			if ($cachedValue !== null) {
907
-				$this->availableTaskTypeIds = $cachedValue;
908
-			}
909
-		}
910
-		// Either we have no cache or showDisabled is turned on, which we don't want to cache, ever.
911
-		if ($this->availableTaskTypeIds === null || $showDisabled) {
912
-			$taskTypes = $this->_getTaskTypes();
913
-			$taskTypeSettings = $this->_getTaskTypeSettings();
914
-
915
-			$availableTaskTypeIds = [];
916
-			foreach ($taskTypes as $taskType) {
917
-				if ((!$showDisabled) && isset($taskTypeSettings[$taskType->getId()]) && !$taskTypeSettings[$taskType->getId()]) {
918
-					continue;
919
-				}
920
-				try {
921
-					$provider = $this->getPreferredProvider($taskType->getId());
922
-				} catch (\OCP\TaskProcessing\Exception\Exception $e) {
923
-					continue;
924
-				}
925
-				$availableTaskTypeIds[] = $taskType->getId();
926
-			}
927
-
928
-			if ($showDisabled) {
929
-				// Do not cache showDisabled, ever.
930
-				return $availableTaskTypeIds;
931
-			}
932
-
933
-			$this->availableTaskTypeIds = $availableTaskTypeIds;
934
-			$this->distributedCache->set(self::TASK_TYPE_IDS_CACHE_KEY, $this->availableTaskTypeIds, 60);
935
-		}
936
-
937
-
938
-		return $this->availableTaskTypeIds;
939
-	}
940
-
941
-	public function canHandleTask(Task $task): bool {
942
-		return isset($this->getAvailableTaskTypes()[$task->getTaskTypeId()]);
943
-	}
944
-
945
-	private function checkGuestAccess(?string $userId = null): bool {
946
-		if ($userId === null && !$this->userSession->isLoggedIn()) {
947
-			return true;
948
-		}
949
-		if ($userId === null) {
950
-			$user = $this->userSession->getUser();
951
-		} else {
952
-			$user = $this->userManager->get($userId);
953
-		}
954
-
955
-		$guestsAllowed = $this->appConfig->getValueString('core', 'ai.taskprocessing_guests', 'false');
956
-		if ($guestsAllowed == 'true' || !class_exists(\OCA\Guests\UserBackend::class) || !($user->getBackend() instanceof \OCA\Guests\UserBackend)) {
957
-			return true;
958
-		}
959
-		return false;
960
-	}
961
-
962
-	public function scheduleTask(Task $task): void {
963
-		if (!$this->checkGuestAccess($task->getUserId())) {
964
-			throw new \OCP\TaskProcessing\Exception\PreConditionNotMetException('Access to this resource is forbidden for guests.');
965
-		}
966
-		if (!$this->canHandleTask($task)) {
967
-			throw new \OCP\TaskProcessing\Exception\PreConditionNotMetException('No task processing provider is installed that can handle this task type: ' . $task->getTaskTypeId());
968
-		}
969
-		$this->prepareTask($task);
970
-		$task->setStatus(Task::STATUS_SCHEDULED);
971
-		$this->storeTask($task);
972
-		// schedule synchronous job if the provider is synchronous
973
-		$provider = $this->getPreferredProvider($task->getTaskTypeId());
974
-		if ($provider instanceof ISynchronousProvider) {
975
-			$this->jobList->add(SynchronousBackgroundJob::class, null);
976
-		}
977
-	}
978
-
979
-	public function runTask(Task $task): Task {
980
-		if (!$this->checkGuestAccess($task->getUserId())) {
981
-			throw new \OCP\TaskProcessing\Exception\PreConditionNotMetException('Access to this resource is forbidden for guests.');
982
-		}
983
-		if (!$this->canHandleTask($task)) {
984
-			throw new \OCP\TaskProcessing\Exception\PreConditionNotMetException('No task processing provider is installed that can handle this task type: ' . $task->getTaskTypeId());
985
-		}
986
-
987
-		$provider = $this->getPreferredProvider($task->getTaskTypeId());
988
-		if ($provider instanceof ISynchronousProvider) {
989
-			$this->prepareTask($task);
990
-			$task->setStatus(Task::STATUS_SCHEDULED);
991
-			$this->storeTask($task);
992
-			$this->processTask($task, $provider);
993
-			$task = $this->getTask($task->getId());
994
-		} else {
995
-			$this->scheduleTask($task);
996
-			// poll task
997
-			while ($task->getStatus() === Task::STATUS_SCHEDULED || $task->getStatus() === Task::STATUS_RUNNING) {
998
-				sleep(1);
999
-				$task = $this->getTask($task->getId());
1000
-			}
1001
-		}
1002
-		return $task;
1003
-	}
1004
-
1005
-	public function processTask(Task $task, ISynchronousProvider $provider): bool {
1006
-		try {
1007
-			try {
1008
-				$input = $this->prepareInputData($task);
1009
-			} catch (GenericFileException|NotPermittedException|LockedException|ValidationException|UnauthorizedException $e) {
1010
-				$this->logger->warning('Failed to prepare input data for a TaskProcessing task with synchronous provider ' . $provider->getId(), ['exception' => $e]);
1011
-				$this->setTaskResult($task->getId(), $e->getMessage(), null);
1012
-				return false;
1013
-			}
1014
-			try {
1015
-				$this->setTaskStatus($task, Task::STATUS_RUNNING);
1016
-				$output = $provider->process($task->getUserId(), $input, fn (float $progress) => $this->setTaskProgress($task->getId(), $progress));
1017
-			} catch (ProcessingException $e) {
1018
-				$this->logger->warning('Failed to process a TaskProcessing task with synchronous provider ' . $provider->getId(), ['exception' => $e]);
1019
-				$this->setTaskResult($task->getId(), $e->getMessage(), null);
1020
-				return false;
1021
-			} catch (\Throwable $e) {
1022
-				$this->logger->error('Unknown error while processing TaskProcessing task', ['exception' => $e]);
1023
-				$this->setTaskResult($task->getId(), $e->getMessage(), null);
1024
-				return false;
1025
-			}
1026
-			$this->setTaskResult($task->getId(), null, $output);
1027
-		} catch (NotFoundException $e) {
1028
-			$this->logger->info('Could not find task anymore after execution. Moving on.', ['exception' => $e]);
1029
-		} catch (Exception $e) {
1030
-			$this->logger->error('Failed to report result of TaskProcessing task', ['exception' => $e]);
1031
-		}
1032
-		return true;
1033
-	}
1034
-
1035
-	public function deleteTask(Task $task): void {
1036
-		$taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
1037
-		$this->taskMapper->delete($taskEntity);
1038
-	}
1039
-
1040
-	public function getTask(int $id): Task {
1041
-		try {
1042
-			$taskEntity = $this->taskMapper->find($id);
1043
-			return $taskEntity->toPublicTask();
1044
-		} catch (DoesNotExistException $e) {
1045
-			throw new NotFoundException('Couldn\'t find task with id ' . $id, 0, $e);
1046
-		} catch (MultipleObjectsReturnedException|\OCP\DB\Exception $e) {
1047
-			throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding the task', 0, $e);
1048
-		} catch (\JsonException $e) {
1049
-			throw new \OCP\TaskProcessing\Exception\Exception('There was a problem parsing JSON after finding the task', 0, $e);
1050
-		}
1051
-	}
1052
-
1053
-	public function cancelTask(int $id): void {
1054
-		$task = $this->getTask($id);
1055
-		if ($task->getStatus() !== Task::STATUS_SCHEDULED && $task->getStatus() !== Task::STATUS_RUNNING) {
1056
-			return;
1057
-		}
1058
-		$task->setStatus(Task::STATUS_CANCELLED);
1059
-		$task->setEndedAt(time());
1060
-		$taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
1061
-		try {
1062
-			$this->taskMapper->update($taskEntity);
1063
-			$this->runWebhook($task);
1064
-		} catch (\OCP\DB\Exception $e) {
1065
-			throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding the task', 0, $e);
1066
-		}
1067
-	}
1068
-
1069
-	public function setTaskProgress(int $id, float $progress): bool {
1070
-		// TODO: Not sure if we should rather catch the exceptions of getTask here and fail silently
1071
-		$task = $this->getTask($id);
1072
-		if ($task->getStatus() === Task::STATUS_CANCELLED) {
1073
-			return false;
1074
-		}
1075
-		// only set the start time if the task is going from scheduled to running
1076
-		if ($task->getstatus() === Task::STATUS_SCHEDULED) {
1077
-			$task->setStartedAt(time());
1078
-		}
1079
-		$task->setStatus(Task::STATUS_RUNNING);
1080
-		$task->setProgress($progress);
1081
-		$taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
1082
-		try {
1083
-			$this->taskMapper->update($taskEntity);
1084
-		} catch (\OCP\DB\Exception $e) {
1085
-			throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding the task', 0, $e);
1086
-		}
1087
-		return true;
1088
-	}
1089
-
1090
-	public function setTaskResult(int $id, ?string $error, ?array $result, bool $isUsingFileIds = false): void {
1091
-		// TODO: Not sure if we should rather catch the exceptions of getTask here and fail silently
1092
-		$task = $this->getTask($id);
1093
-		if ($task->getStatus() === Task::STATUS_CANCELLED) {
1094
-			$this->logger->info('A TaskProcessing ' . $task->getTaskTypeId() . ' task with id ' . $id . ' finished but was cancelled in the mean time. Moving on without storing result.');
1095
-			return;
1096
-		}
1097
-		if ($error !== null) {
1098
-			$task->setStatus(Task::STATUS_FAILED);
1099
-			$task->setEndedAt(time());
1100
-			// truncate error message to 1000 characters
1101
-			$task->setErrorMessage(mb_substr($error, 0, 1000));
1102
-			$this->logger->warning('A TaskProcessing ' . $task->getTaskTypeId() . ' task with id ' . $id . ' failed with the following message: ' . $error);
1103
-		} elseif ($result !== null) {
1104
-			$taskTypes = $this->getAvailableTaskTypes();
1105
-			$outputShape = $taskTypes[$task->getTaskTypeId()]['outputShape'];
1106
-			$outputShapeEnumValues = $taskTypes[$task->getTaskTypeId()]['outputShapeEnumValues'];
1107
-			$optionalOutputShape = $taskTypes[$task->getTaskTypeId()]['optionalOutputShape'];
1108
-			$optionalOutputShapeEnumValues = $taskTypes[$task->getTaskTypeId()]['optionalOutputShapeEnumValues'];
1109
-			try {
1110
-				// validate output
1111
-				if (!$isUsingFileIds) {
1112
-					$this->validateOutputWithFileData($outputShape, $outputShapeEnumValues, $result);
1113
-					$this->validateOutputWithFileData($optionalOutputShape, $optionalOutputShapeEnumValues, $result, true);
1114
-				} else {
1115
-					$this->validateOutputWithFileIds($outputShape, $outputShapeEnumValues, $result);
1116
-					$this->validateOutputWithFileIds($optionalOutputShape, $optionalOutputShapeEnumValues, $result, true);
1117
-				}
1118
-				$output = $this->removeSuperfluousArrayKeys($result, $outputShape, $optionalOutputShape);
1119
-				// extract raw data and put it in files, replace it with file ids
1120
-				if (!$isUsingFileIds) {
1121
-					$output = $this->encapsulateOutputFileData($output, $outputShape, $optionalOutputShape);
1122
-				} else {
1123
-					$this->validateOutputFileIds($output, $outputShape, $optionalOutputShape);
1124
-				}
1125
-				// Turn file objects into IDs
1126
-				foreach ($output as $key => $value) {
1127
-					if ($value instanceof Node) {
1128
-						$output[$key] = $value->getId();
1129
-					}
1130
-					if (is_array($value) && isset($value[0]) && $value[0] instanceof Node) {
1131
-						$output[$key] = array_map(fn ($node) => $node->getId(), $value);
1132
-					}
1133
-				}
1134
-				$task->setOutput($output);
1135
-				$task->setProgress(1);
1136
-				$task->setStatus(Task::STATUS_SUCCESSFUL);
1137
-				$task->setEndedAt(time());
1138
-			} catch (ValidationException $e) {
1139
-				$task->setProgress(1);
1140
-				$task->setStatus(Task::STATUS_FAILED);
1141
-				$task->setEndedAt(time());
1142
-				$error = 'The task was processed successfully but the provider\'s output doesn\'t pass validation against the task type\'s outputShape spec and/or the provider\'s own optionalOutputShape spec';
1143
-				$task->setErrorMessage($error);
1144
-				$this->logger->error($error, ['exception' => $e, 'output' => $result]);
1145
-			} catch (NotPermittedException $e) {
1146
-				$task->setProgress(1);
1147
-				$task->setStatus(Task::STATUS_FAILED);
1148
-				$task->setEndedAt(time());
1149
-				$error = 'The task was processed successfully but storing the output in a file failed';
1150
-				$task->setErrorMessage($error);
1151
-				$this->logger->error($error, ['exception' => $e]);
1152
-			} catch (InvalidPathException|\OCP\Files\NotFoundException $e) {
1153
-				$task->setProgress(1);
1154
-				$task->setStatus(Task::STATUS_FAILED);
1155
-				$task->setEndedAt(time());
1156
-				$error = 'The task was processed successfully but the result file could not be found';
1157
-				$task->setErrorMessage($error);
1158
-				$this->logger->error($error, ['exception' => $e]);
1159
-			}
1160
-		}
1161
-		try {
1162
-			$taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
1163
-		} catch (\JsonException $e) {
1164
-			throw new \OCP\TaskProcessing\Exception\Exception('The task was processed successfully but the provider\'s output could not be encoded as JSON for the database.', 0, $e);
1165
-		}
1166
-		try {
1167
-			$this->taskMapper->update($taskEntity);
1168
-			$this->runWebhook($task);
1169
-		} catch (\OCP\DB\Exception $e) {
1170
-			throw new \OCP\TaskProcessing\Exception\Exception($e->getMessage());
1171
-		}
1172
-		if ($task->getStatus() === Task::STATUS_SUCCESSFUL) {
1173
-			$event = new TaskSuccessfulEvent($task);
1174
-		} else {
1175
-			$event = new TaskFailedEvent($task, $error);
1176
-		}
1177
-		$this->dispatcher->dispatchTyped($event);
1178
-	}
1179
-
1180
-	public function getNextScheduledTask(array $taskTypeIds = [], array $taskIdsToIgnore = []): Task {
1181
-		try {
1182
-			$taskEntity = $this->taskMapper->findOldestScheduledByType($taskTypeIds, $taskIdsToIgnore);
1183
-			return $taskEntity->toPublicTask();
1184
-		} catch (DoesNotExistException $e) {
1185
-			throw new \OCP\TaskProcessing\Exception\NotFoundException('Could not find the task', 0, $e);
1186
-		} catch (\OCP\DB\Exception $e) {
1187
-			throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding the task', 0, $e);
1188
-		} catch (\JsonException $e) {
1189
-			throw new \OCP\TaskProcessing\Exception\Exception('There was a problem parsing JSON after finding the task', 0, $e);
1190
-		}
1191
-	}
1192
-
1193
-	/**
1194
-	 * Takes task input data and replaces fileIds with File objects
1195
-	 *
1196
-	 * @param string|null $userId
1197
-	 * @param array<array-key, list<numeric|string>|numeric|string> $input
1198
-	 * @param ShapeDescriptor[] ...$specs the specs
1199
-	 * @return array<array-key, list<File|numeric|string>|numeric|string|File>
1200
-	 * @throws GenericFileException|LockedException|NotPermittedException|ValidationException|UnauthorizedException
1201
-	 */
1202
-	public function fillInputFileData(?string $userId, array $input, ...$specs): array {
1203
-		if ($userId !== null) {
1204
-			\OC_Util::setupFS($userId);
1205
-		}
1206
-		$newInputOutput = [];
1207
-		$spec = array_reduce($specs, fn ($carry, $spec) => $carry + $spec, []);
1208
-		foreach ($spec as $key => $descriptor) {
1209
-			$type = $descriptor->getShapeType();
1210
-			if (!isset($input[$key])) {
1211
-				continue;
1212
-			}
1213
-			if (!in_array(EShapeType::getScalarType($type), [EShapeType::Image, EShapeType::Audio, EShapeType::Video, EShapeType::File], true)) {
1214
-				$newInputOutput[$key] = $input[$key];
1215
-				continue;
1216
-			}
1217
-			if (EShapeType::getScalarType($type) === $type) {
1218
-				// is scalar
1219
-				$node = $this->validateFileId((int)$input[$key]);
1220
-				$this->validateUserAccessToFile($input[$key], $userId);
1221
-				$newInputOutput[$key] = $node;
1222
-			} else {
1223
-				// is list
1224
-				$newInputOutput[$key] = [];
1225
-				foreach ($input[$key] as $item) {
1226
-					$node = $this->validateFileId((int)$item);
1227
-					$this->validateUserAccessToFile($item, $userId);
1228
-					$newInputOutput[$key][] = $node;
1229
-				}
1230
-			}
1231
-		}
1232
-		return $newInputOutput;
1233
-	}
1234
-
1235
-	public function getUserTask(int $id, ?string $userId): Task {
1236
-		try {
1237
-			$taskEntity = $this->taskMapper->findByIdAndUser($id, $userId);
1238
-			return $taskEntity->toPublicTask();
1239
-		} catch (DoesNotExistException $e) {
1240
-			throw new \OCP\TaskProcessing\Exception\NotFoundException('Could not find the task', 0, $e);
1241
-		} catch (MultipleObjectsReturnedException|\OCP\DB\Exception $e) {
1242
-			throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding the task', 0, $e);
1243
-		} catch (\JsonException $e) {
1244
-			throw new \OCP\TaskProcessing\Exception\Exception('There was a problem parsing JSON after finding the task', 0, $e);
1245
-		}
1246
-	}
1247
-
1248
-	public function getUserTasks(?string $userId, ?string $taskTypeId = null, ?string $customId = null): array {
1249
-		try {
1250
-			$taskEntities = $this->taskMapper->findByUserAndTaskType($userId, $taskTypeId, $customId);
1251
-			return array_map(fn ($taskEntity): Task => $taskEntity->toPublicTask(), $taskEntities);
1252
-		} catch (\OCP\DB\Exception $e) {
1253
-			throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding the tasks', 0, $e);
1254
-		} catch (\JsonException $e) {
1255
-			throw new \OCP\TaskProcessing\Exception\Exception('There was a problem parsing JSON after finding the tasks', 0, $e);
1256
-		}
1257
-	}
1258
-
1259
-	public function getTasks(
1260
-		?string $userId, ?string $taskTypeId = null, ?string $appId = null, ?string $customId = null,
1261
-		?int $status = null, ?int $scheduleAfter = null, ?int $endedBefore = null,
1262
-	): array {
1263
-		try {
1264
-			$taskEntities = $this->taskMapper->findTasks($userId, $taskTypeId, $appId, $customId, $status, $scheduleAfter, $endedBefore);
1265
-			return array_map(fn ($taskEntity): Task => $taskEntity->toPublicTask(), $taskEntities);
1266
-		} catch (\OCP\DB\Exception $e) {
1267
-			throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding the tasks', 0, $e);
1268
-		} catch (\JsonException $e) {
1269
-			throw new \OCP\TaskProcessing\Exception\Exception('There was a problem parsing JSON after finding the tasks', 0, $e);
1270
-		}
1271
-	}
1272
-
1273
-	public function getUserTasksByApp(?string $userId, string $appId, ?string $customId = null): array {
1274
-		try {
1275
-			$taskEntities = $this->taskMapper->findUserTasksByApp($userId, $appId, $customId);
1276
-			return array_map(fn ($taskEntity): Task => $taskEntity->toPublicTask(), $taskEntities);
1277
-		} catch (\OCP\DB\Exception $e) {
1278
-			throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding a task', 0, $e);
1279
-		} catch (\JsonException $e) {
1280
-			throw new \OCP\TaskProcessing\Exception\Exception('There was a problem parsing JSON after finding a task', 0, $e);
1281
-		}
1282
-	}
1283
-
1284
-	/**
1285
-	 *Takes task input or output and replaces base64 data with file ids
1286
-	 *
1287
-	 * @param array $output
1288
-	 * @param ShapeDescriptor[] ...$specs the specs that define which keys to keep
1289
-	 * @return array
1290
-	 * @throws NotPermittedException
1291
-	 */
1292
-	public function encapsulateOutputFileData(array $output, ...$specs): array {
1293
-		$newOutput = [];
1294
-		try {
1295
-			$folder = $this->appData->getFolder('TaskProcessing');
1296
-		} catch (\OCP\Files\NotFoundException) {
1297
-			$folder = $this->appData->newFolder('TaskProcessing');
1298
-		}
1299
-		$spec = array_reduce($specs, fn ($carry, $spec) => $carry + $spec, []);
1300
-		foreach ($spec as $key => $descriptor) {
1301
-			$type = $descriptor->getShapeType();
1302
-			if (!isset($output[$key])) {
1303
-				continue;
1304
-			}
1305
-			if (!in_array(EShapeType::getScalarType($type), [EShapeType::Image, EShapeType::Audio, EShapeType::Video, EShapeType::File], true)) {
1306
-				$newOutput[$key] = $output[$key];
1307
-				continue;
1308
-			}
1309
-			if (EShapeType::getScalarType($type) === $type) {
1310
-				/** @var SimpleFile $file */
1311
-				$file = $folder->newFile(time() . '-' . rand(1, 100000), $output[$key]);
1312
-				$newOutput[$key] = $file->getId(); // polymorphic call to SimpleFile
1313
-			} else {
1314
-				$newOutput = [];
1315
-				foreach ($output[$key] as $item) {
1316
-					/** @var SimpleFile $file */
1317
-					$file = $folder->newFile(time() . '-' . rand(1, 100000), $item);
1318
-					$newOutput[$key][] = $file->getId();
1319
-				}
1320
-			}
1321
-		}
1322
-		return $newOutput;
1323
-	}
1324
-
1325
-	/**
1326
-	 * @param Task $task
1327
-	 * @return array<array-key, list<numeric|string|File>|numeric|string|File>
1328
-	 * @throws GenericFileException
1329
-	 * @throws LockedException
1330
-	 * @throws NotPermittedException
1331
-	 * @throws ValidationException|UnauthorizedException
1332
-	 */
1333
-	public function prepareInputData(Task $task): array {
1334
-		$taskTypes = $this->getAvailableTaskTypes();
1335
-		$inputShape = $taskTypes[$task->getTaskTypeId()]['inputShape'];
1336
-		$optionalInputShape = $taskTypes[$task->getTaskTypeId()]['optionalInputShape'];
1337
-		$input = $task->getInput();
1338
-		$input = $this->removeSuperfluousArrayKeys($input, $inputShape, $optionalInputShape);
1339
-		$input = $this->fillInputFileData($task->getUserId(), $input, $inputShape, $optionalInputShape);
1340
-		return $input;
1341
-	}
1342
-
1343
-	public function lockTask(Task $task): bool {
1344
-		$taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
1345
-		if ($this->taskMapper->lockTask($taskEntity) === 0) {
1346
-			return false;
1347
-		}
1348
-		$task->setStatus(Task::STATUS_RUNNING);
1349
-		return true;
1350
-	}
1351
-
1352
-	/**
1353
-	 * @throws \JsonException
1354
-	 * @throws Exception
1355
-	 */
1356
-	public function setTaskStatus(Task $task, int $status): void {
1357
-		$currentTaskStatus = $task->getStatus();
1358
-		if ($currentTaskStatus === Task::STATUS_SCHEDULED && $status === Task::STATUS_RUNNING) {
1359
-			$task->setStartedAt(time());
1360
-		} elseif ($currentTaskStatus === Task::STATUS_RUNNING && ($status === Task::STATUS_FAILED || $status === Task::STATUS_CANCELLED)) {
1361
-			$task->setEndedAt(time());
1362
-		} elseif ($currentTaskStatus === Task::STATUS_UNKNOWN && $status === Task::STATUS_SCHEDULED) {
1363
-			$task->setScheduledAt(time());
1364
-		}
1365
-		$task->setStatus($status);
1366
-		$taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
1367
-		$this->taskMapper->update($taskEntity);
1368
-	}
1369
-
1370
-	/**
1371
-	 * Validate input, fill input default values, set completionExpectedAt, set scheduledAt
1372
-	 *
1373
-	 * @param Task $task
1374
-	 * @return void
1375
-	 * @throws UnauthorizedException
1376
-	 * @throws ValidationException
1377
-	 * @throws \OCP\TaskProcessing\Exception\Exception
1378
-	 */
1379
-	private function prepareTask(Task $task): void {
1380
-		$taskTypes = $this->getAvailableTaskTypes();
1381
-		$taskType = $taskTypes[$task->getTaskTypeId()];
1382
-		$inputShape = $taskType['inputShape'];
1383
-		$inputShapeDefaults = $taskType['inputShapeDefaults'];
1384
-		$inputShapeEnumValues = $taskType['inputShapeEnumValues'];
1385
-		$optionalInputShape = $taskType['optionalInputShape'];
1386
-		$optionalInputShapeEnumValues = $taskType['optionalInputShapeEnumValues'];
1387
-		$optionalInputShapeDefaults = $taskType['optionalInputShapeDefaults'];
1388
-		// validate input
1389
-		$this->validateInput($inputShape, $inputShapeDefaults, $inputShapeEnumValues, $task->getInput());
1390
-		$this->validateInput($optionalInputShape, $optionalInputShapeDefaults, $optionalInputShapeEnumValues, $task->getInput(), true);
1391
-		// authenticate access to mentioned files
1392
-		$ids = [];
1393
-		foreach ($inputShape + $optionalInputShape as $key => $descriptor) {
1394
-			if (in_array(EShapeType::getScalarType($descriptor->getShapeType()), [EShapeType::File, EShapeType::Image, EShapeType::Audio, EShapeType::Video], true)) {
1395
-				/** @var list<int>|int $inputSlot */
1396
-				$inputSlot = $task->getInput()[$key];
1397
-				if (is_array($inputSlot)) {
1398
-					$ids += $inputSlot;
1399
-				} else {
1400
-					$ids[] = $inputSlot;
1401
-				}
1402
-			}
1403
-		}
1404
-		foreach ($ids as $fileId) {
1405
-			$this->validateFileId($fileId);
1406
-			$this->validateUserAccessToFile($fileId, $task->getUserId());
1407
-		}
1408
-		// remove superfluous keys and set input
1409
-		$input = $this->removeSuperfluousArrayKeys($task->getInput(), $inputShape, $optionalInputShape);
1410
-		$inputWithDefaults = $this->fillInputDefaults($input, $inputShapeDefaults, $optionalInputShapeDefaults);
1411
-		$task->setInput($inputWithDefaults);
1412
-		$task->setScheduledAt(time());
1413
-		$provider = $this->getPreferredProvider($task->getTaskTypeId());
1414
-		// calculate expected completion time
1415
-		$completionExpectedAt = new \DateTime('now');
1416
-		$completionExpectedAt->add(new \DateInterval('PT' . $provider->getExpectedRuntime() . 'S'));
1417
-		$task->setCompletionExpectedAt($completionExpectedAt);
1418
-	}
1419
-
1420
-	/**
1421
-	 * Store the task in the DB and set its ID in the \OCP\TaskProcessing\Task input param
1422
-	 *
1423
-	 * @param Task $task
1424
-	 * @return void
1425
-	 * @throws Exception
1426
-	 * @throws \JsonException
1427
-	 */
1428
-	private function storeTask(Task $task): void {
1429
-		// create a db entity and insert into db table
1430
-		$taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
1431
-		$this->taskMapper->insert($taskEntity);
1432
-		// make sure the scheduler knows the id
1433
-		$task->setId($taskEntity->getId());
1434
-	}
1435
-
1436
-	/**
1437
-	 * @param array $output
1438
-	 * @param ShapeDescriptor[] ...$specs the specs that define which keys to keep
1439
-	 * @return array
1440
-	 * @throws NotPermittedException
1441
-	 */
1442
-	private function validateOutputFileIds(array $output, ...$specs): array {
1443
-		$newOutput = [];
1444
-		$spec = array_reduce($specs, fn ($carry, $spec) => $carry + $spec, []);
1445
-		foreach ($spec as $key => $descriptor) {
1446
-			$type = $descriptor->getShapeType();
1447
-			if (!isset($output[$key])) {
1448
-				continue;
1449
-			}
1450
-			if (!in_array(EShapeType::getScalarType($type), [EShapeType::Image, EShapeType::Audio, EShapeType::Video, EShapeType::File], true)) {
1451
-				$newOutput[$key] = $output[$key];
1452
-				continue;
1453
-			}
1454
-			if (EShapeType::getScalarType($type) === $type) {
1455
-				// Is scalar file ID
1456
-				$newOutput[$key] = $this->validateFileId($output[$key]);
1457
-			} else {
1458
-				// Is list of file IDs
1459
-				$newOutput = [];
1460
-				foreach ($output[$key] as $item) {
1461
-					$newOutput[$key][] = $this->validateFileId($item);
1462
-				}
1463
-			}
1464
-		}
1465
-		return $newOutput;
1466
-	}
1467
-
1468
-	/**
1469
-	 * @param mixed $id
1470
-	 * @return File
1471
-	 * @throws ValidationException
1472
-	 */
1473
-	private function validateFileId(mixed $id): File {
1474
-		$node = $this->rootFolder->getFirstNodeById($id);
1475
-		if ($node === null) {
1476
-			$node = $this->rootFolder->getFirstNodeByIdInPath($id, '/' . $this->rootFolder->getAppDataDirectoryName() . '/');
1477
-			if ($node === null) {
1478
-				throw new ValidationException('Could not find file ' . $id);
1479
-			} elseif (!$node instanceof File) {
1480
-				throw new ValidationException('File with id "' . $id . '" is not a file');
1481
-			}
1482
-		} elseif (!$node instanceof File) {
1483
-			throw new ValidationException('File with id "' . $id . '" is not a file');
1484
-		}
1485
-		return $node;
1486
-	}
1487
-
1488
-	/**
1489
-	 * @param mixed $fileId
1490
-	 * @param string|null $userId
1491
-	 * @return void
1492
-	 * @throws UnauthorizedException
1493
-	 */
1494
-	private function validateUserAccessToFile(mixed $fileId, ?string $userId): void {
1495
-		if ($userId === null) {
1496
-			throw new UnauthorizedException('User does not have access to file ' . $fileId);
1497
-		}
1498
-		$mounts = $this->userMountCache->getMountsForFileId($fileId);
1499
-		$userIds = array_map(fn ($mount) => $mount->getUser()->getUID(), $mounts);
1500
-		if (!in_array($userId, $userIds)) {
1501
-			throw new UnauthorizedException('User ' . $userId . ' does not have access to file ' . $fileId);
1502
-		}
1503
-	}
1504
-
1505
-	/**
1506
-	 * @param Task $task
1507
-	 * @return list<int>
1508
-	 * @throws NotFoundException
1509
-	 */
1510
-	public function extractFileIdsFromTask(Task $task): array {
1511
-		$ids = [];
1512
-		$taskTypes = $this->getAvailableTaskTypes();
1513
-		if (!isset($taskTypes[$task->getTaskTypeId()])) {
1514
-			throw new NotFoundException('Could not find task type');
1515
-		}
1516
-		$taskType = $taskTypes[$task->getTaskTypeId()];
1517
-		foreach ($taskType['inputShape'] + $taskType['optionalInputShape'] as $key => $descriptor) {
1518
-			if (in_array(EShapeType::getScalarType($descriptor->getShapeType()), [EShapeType::File, EShapeType::Image, EShapeType::Audio, EShapeType::Video], true)) {
1519
-				/** @var int|list<int> $inputSlot */
1520
-				$inputSlot = $task->getInput()[$key];
1521
-				if (is_array($inputSlot)) {
1522
-					$ids = array_merge($inputSlot, $ids);
1523
-				} else {
1524
-					$ids[] = $inputSlot;
1525
-				}
1526
-			}
1527
-		}
1528
-		if ($task->getOutput() !== null) {
1529
-			foreach ($taskType['outputShape'] + $taskType['optionalOutputShape'] as $key => $descriptor) {
1530
-				if (in_array(EShapeType::getScalarType($descriptor->getShapeType()), [EShapeType::File, EShapeType::Image, EShapeType::Audio, EShapeType::Video], true)) {
1531
-					/** @var int|list<int> $outputSlot */
1532
-					$outputSlot = $task->getOutput()[$key];
1533
-					if (is_array($outputSlot)) {
1534
-						$ids = array_merge($outputSlot, $ids);
1535
-					} else {
1536
-						$ids[] = $outputSlot;
1537
-					}
1538
-				}
1539
-			}
1540
-		}
1541
-		return $ids;
1542
-	}
1543
-
1544
-	/**
1545
-	 * @param ISimpleFolder $folder
1546
-	 * @param int $ageInSeconds
1547
-	 * @return \Generator
1548
-	 */
1549
-	public function clearFilesOlderThan(ISimpleFolder $folder, int $ageInSeconds = self::MAX_TASK_AGE_SECONDS): \Generator {
1550
-		foreach ($folder->getDirectoryListing() as $file) {
1551
-			if ($file->getMTime() < time() - $ageInSeconds) {
1552
-				try {
1553
-					$fileName = $file->getName();
1554
-					$file->delete();
1555
-					yield $fileName;
1556
-				} catch (NotPermittedException $e) {
1557
-					$this->logger->warning('Failed to delete a stale task processing file', ['exception' => $e]);
1558
-				}
1559
-			}
1560
-		}
1561
-	}
1562
-
1563
-	/**
1564
-	 * @param int $ageInSeconds
1565
-	 * @return \Generator
1566
-	 * @throws Exception
1567
-	 * @throws InvalidPathException
1568
-	 * @throws NotFoundException
1569
-	 * @throws \JsonException
1570
-	 * @throws \OCP\Files\NotFoundException
1571
-	 */
1572
-	public function cleanupTaskProcessingTaskFiles(int $ageInSeconds = self::MAX_TASK_AGE_SECONDS): \Generator {
1573
-		$taskIdsToCleanup = [];
1574
-		foreach ($this->taskMapper->getTasksToCleanup($ageInSeconds) as $task) {
1575
-			$taskIdsToCleanup[] = $task->getId();
1576
-			$ocpTask = $task->toPublicTask();
1577
-			$fileIds = $this->extractFileIdsFromTask($ocpTask);
1578
-			foreach ($fileIds as $fileId) {
1579
-				// only look for output files stored in appData/TaskProcessing/
1580
-				$file = $this->rootFolder->getFirstNodeByIdInPath($fileId, '/' . $this->rootFolder->getAppDataDirectoryName() . '/core/TaskProcessing/');
1581
-				if ($file instanceof File) {
1582
-					try {
1583
-						$fileId = $file->getId();
1584
-						$fileName = $file->getName();
1585
-						$file->delete();
1586
-						yield ['task_id' => $task->getId(), 'file_id' => $fileId, 'file_name' => $fileName];
1587
-					} catch (NotPermittedException $e) {
1588
-						$this->logger->warning('Failed to delete a stale task processing file', ['exception' => $e]);
1589
-					}
1590
-				}
1591
-			}
1592
-		}
1593
-		return $taskIdsToCleanup;
1594
-	}
1595
-
1596
-	/**
1597
-	 * Make a request to the task's webhookUri if necessary
1598
-	 *
1599
-	 * @param Task $task
1600
-	 */
1601
-	private function runWebhook(Task $task): void {
1602
-		$uri = $task->getWebhookUri();
1603
-		$method = $task->getWebhookMethod();
1604
-
1605
-		if (!$uri || !$method) {
1606
-			return;
1607
-		}
1608
-
1609
-		if (in_array($method, ['HTTP:GET', 'HTTP:POST', 'HTTP:PUT', 'HTTP:DELETE'], true)) {
1610
-			$client = $this->clientService->newClient();
1611
-			$httpMethod = preg_replace('/^HTTP:/', '', $method);
1612
-			$options = [
1613
-				'timeout' => 30,
1614
-				'body' => json_encode([
1615
-					'task' => $task->jsonSerialize(),
1616
-				]),
1617
-				'headers' => ['Content-Type' => 'application/json'],
1618
-			];
1619
-			try {
1620
-				$client->request($httpMethod, $uri, $options);
1621
-			} catch (ClientException|ServerException $e) {
1622
-				$this->logger->warning('Task processing HTTP webhook failed for task ' . $task->getId() . '. Request failed', ['exception' => $e]);
1623
-			} catch (\Exception|\Throwable $e) {
1624
-				$this->logger->warning('Task processing HTTP webhook failed for task ' . $task->getId() . '. Unknown error', ['exception' => $e]);
1625
-			}
1626
-		} elseif (str_starts_with($method, 'AppAPI:') && str_starts_with($uri, '/')) {
1627
-			$parsedMethod = explode(':', $method, 4);
1628
-			if (count($parsedMethod) < 3) {
1629
-				$this->logger->warning('Task processing AppAPI webhook failed for task ' . $task->getId() . '. Invalid method: ' . $method);
1630
-			}
1631
-			[, $exAppId, $httpMethod] = $parsedMethod;
1632
-			if (!$this->appManager->isEnabledForAnyone('app_api')) {
1633
-				$this->logger->warning('Task processing AppAPI webhook failed for task ' . $task->getId() . '. AppAPI is disabled or not installed.');
1634
-				return;
1635
-			}
1636
-			try {
1637
-				$appApiFunctions = \OCP\Server::get(\OCA\AppAPI\PublicFunctions::class);
1638
-			} catch (ContainerExceptionInterface|NotFoundExceptionInterface) {
1639
-				$this->logger->warning('Task processing AppAPI webhook failed for task ' . $task->getId() . '. Could not get AppAPI public functions.');
1640
-				return;
1641
-			}
1642
-			$exApp = $appApiFunctions->getExApp($exAppId);
1643
-			if ($exApp === null) {
1644
-				$this->logger->warning('Task processing AppAPI webhook failed for task ' . $task->getId() . '. ExApp ' . $exAppId . ' is missing.');
1645
-				return;
1646
-			} elseif (!$exApp['enabled']) {
1647
-				$this->logger->warning('Task processing AppAPI webhook failed for task ' . $task->getId() . '. ExApp ' . $exAppId . ' is disabled.');
1648
-				return;
1649
-			}
1650
-			$requestParams = [
1651
-				'task' => $task->jsonSerialize(),
1652
-			];
1653
-			$requestOptions = [
1654
-				'timeout' => 30,
1655
-			];
1656
-			$response = $appApiFunctions->exAppRequest($exAppId, $uri, $task->getUserId(), $httpMethod, $requestParams, $requestOptions);
1657
-			if (is_array($response) && isset($response['error'])) {
1658
-				$this->logger->warning('Task processing AppAPI webhook failed for task ' . $task->getId() . '. Error during request to ExApp(' . $exAppId . '): ', $response['error']);
1659
-			}
1660
-		}
1661
-	}
73
+    public const LEGACY_PREFIX_TEXTPROCESSING = 'legacy:TextProcessing:';
74
+    public const LEGACY_PREFIX_TEXTTOIMAGE = 'legacy:TextToImage:';
75
+    public const LEGACY_PREFIX_SPEECHTOTEXT = 'legacy:SpeechToText:';
76
+
77
+    public const LAZY_CONFIG_KEYS = [
78
+        'ai.taskprocessing_type_preferences',
79
+        'ai.taskprocessing_provider_preferences',
80
+    ];
81
+
82
+    public const MAX_TASK_AGE_SECONDS = 60 * 60 * 24 * 31 * 6; // 6 months
83
+
84
+    private const TASK_TYPES_CACHE_KEY = 'available_task_types_v3';
85
+    private const TASK_TYPE_IDS_CACHE_KEY = 'available_task_type_ids';
86
+
87
+    /** @var list<IProvider>|null */
88
+    private ?array $providers = null;
89
+
90
+    /**
91
+     * @var array<array-key,array{name: string, description: string, inputShape: ShapeDescriptor[], inputShapeEnumValues: ShapeEnumValue[][], inputShapeDefaults: array<array-key, numeric|string>, optionalInputShape: ShapeDescriptor[], optionalInputShapeEnumValues: ShapeEnumValue[][], optionalInputShapeDefaults: array<array-key, numeric|string>, outputShape: ShapeDescriptor[], outputShapeEnumValues: ShapeEnumValue[][], optionalOutputShape: ShapeDescriptor[], optionalOutputShapeEnumValues: ShapeEnumValue[][]}>
92
+     */
93
+    private ?array $availableTaskTypes = null;
94
+
95
+    /** @var list<string>|null */
96
+    private ?array $availableTaskTypeIds = null;
97
+
98
+    private IAppData $appData;
99
+    private ?array $preferences = null;
100
+    private ?array $providersById = null;
101
+
102
+    /** @var ITaskType[]|null */
103
+    private ?array $taskTypes = null;
104
+    private ICache $distributedCache;
105
+
106
+    private ?GetTaskProcessingProvidersEvent $eventResult = null;
107
+
108
+    public function __construct(
109
+        private IAppConfig $appConfig,
110
+        private Coordinator $coordinator,
111
+        private IServerContainer $serverContainer,
112
+        private LoggerInterface $logger,
113
+        private TaskMapper $taskMapper,
114
+        private IJobList $jobList,
115
+        private IEventDispatcher $dispatcher,
116
+        IAppDataFactory $appDataFactory,
117
+        private IRootFolder $rootFolder,
118
+        private \OCP\TextToImage\IManager $textToImageManager,
119
+        private IUserMountCache $userMountCache,
120
+        private IClientService $clientService,
121
+        private IAppManager $appManager,
122
+        private IUserManager $userManager,
123
+        private IUserSession $userSession,
124
+        ICacheFactory $cacheFactory,
125
+        private IFactory $l10nFactory,
126
+    ) {
127
+        $this->appData = $appDataFactory->get('core');
128
+        $this->distributedCache = $cacheFactory->createDistributed('task_processing::');
129
+    }
130
+
131
+
132
+    /**
133
+     * This is almost a copy of textProcessingManager->getProviders
134
+     * to avoid a dependency cycle between TextProcessingManager and TaskProcessingManager
135
+     */
136
+    private function _getRawTextProcessingProviders(): array {
137
+        $context = $this->coordinator->getRegistrationContext();
138
+        if ($context === null) {
139
+            return [];
140
+        }
141
+
142
+        $providers = [];
143
+
144
+        foreach ($context->getTextProcessingProviders() as $providerServiceRegistration) {
145
+            $class = $providerServiceRegistration->getService();
146
+            try {
147
+                $providers[$class] = $this->serverContainer->get($class);
148
+            } catch (\Throwable $e) {
149
+                $this->logger->error('Failed to load Text processing provider ' . $class, [
150
+                    'exception' => $e,
151
+                ]);
152
+            }
153
+        }
154
+
155
+        return $providers;
156
+    }
157
+
158
+    private function _getTextProcessingProviders(): array {
159
+        $oldProviders = $this->_getRawTextProcessingProviders();
160
+        $newProviders = [];
161
+        foreach ($oldProviders as $oldProvider) {
162
+            $provider = new class($oldProvider) implements IProvider, ISynchronousProvider {
163
+                private \OCP\TextProcessing\IProvider $provider;
164
+
165
+                public function __construct(\OCP\TextProcessing\IProvider $provider) {
166
+                    $this->provider = $provider;
167
+                }
168
+
169
+                public function getId(): string {
170
+                    if ($this->provider instanceof \OCP\TextProcessing\IProviderWithId) {
171
+                        return $this->provider->getId();
172
+                    }
173
+                    return Manager::LEGACY_PREFIX_TEXTPROCESSING . $this->provider::class;
174
+                }
175
+
176
+                public function getName(): string {
177
+                    return $this->provider->getName();
178
+                }
179
+
180
+                public function getTaskTypeId(): string {
181
+                    return match ($this->provider->getTaskType()) {
182
+                        \OCP\TextProcessing\FreePromptTaskType::class => TextToText::ID,
183
+                        \OCP\TextProcessing\HeadlineTaskType::class => TextToTextHeadline::ID,
184
+                        \OCP\TextProcessing\TopicsTaskType::class => TextToTextTopics::ID,
185
+                        \OCP\TextProcessing\SummaryTaskType::class => TextToTextSummary::ID,
186
+                        default => Manager::LEGACY_PREFIX_TEXTPROCESSING . $this->provider->getTaskType(),
187
+                    };
188
+                }
189
+
190
+                public function getExpectedRuntime(): int {
191
+                    if ($this->provider instanceof \OCP\TextProcessing\IProviderWithExpectedRuntime) {
192
+                        return $this->provider->getExpectedRuntime();
193
+                    }
194
+                    return 60;
195
+                }
196
+
197
+                public function getOptionalInputShape(): array {
198
+                    return [];
199
+                }
200
+
201
+                public function getOptionalOutputShape(): array {
202
+                    return [];
203
+                }
204
+
205
+                public function process(?string $userId, array $input, callable $reportProgress): array {
206
+                    if ($this->provider instanceof \OCP\TextProcessing\IProviderWithUserId) {
207
+                        $this->provider->setUserId($userId);
208
+                    }
209
+                    try {
210
+                        return ['output' => $this->provider->process($input['input'])];
211
+                    } catch (\RuntimeException $e) {
212
+                        throw new ProcessingException($e->getMessage(), 0, $e);
213
+                    }
214
+                }
215
+
216
+                public function getInputShapeEnumValues(): array {
217
+                    return [];
218
+                }
219
+
220
+                public function getInputShapeDefaults(): array {
221
+                    return [];
222
+                }
223
+
224
+                public function getOptionalInputShapeEnumValues(): array {
225
+                    return [];
226
+                }
227
+
228
+                public function getOptionalInputShapeDefaults(): array {
229
+                    return [];
230
+                }
231
+
232
+                public function getOutputShapeEnumValues(): array {
233
+                    return [];
234
+                }
235
+
236
+                public function getOptionalOutputShapeEnumValues(): array {
237
+                    return [];
238
+                }
239
+            };
240
+            $newProviders[$provider->getId()] = $provider;
241
+        }
242
+
243
+        return $newProviders;
244
+    }
245
+
246
+    /**
247
+     * @return ITaskType[]
248
+     */
249
+    private function _getTextProcessingTaskTypes(): array {
250
+        $oldProviders = $this->_getRawTextProcessingProviders();
251
+        $newTaskTypes = [];
252
+        foreach ($oldProviders as $oldProvider) {
253
+            // These are already implemented in the TaskProcessing realm
254
+            if (in_array($oldProvider->getTaskType(), [
255
+                \OCP\TextProcessing\FreePromptTaskType::class,
256
+                \OCP\TextProcessing\HeadlineTaskType::class,
257
+                \OCP\TextProcessing\TopicsTaskType::class,
258
+                \OCP\TextProcessing\SummaryTaskType::class
259
+            ], true)) {
260
+                continue;
261
+            }
262
+            $taskType = new class($oldProvider->getTaskType()) implements ITaskType {
263
+                private string $oldTaskTypeClass;
264
+                private \OCP\TextProcessing\ITaskType $oldTaskType;
265
+                private IL10N $l;
266
+
267
+                public function __construct(string $oldTaskTypeClass) {
268
+                    $this->oldTaskTypeClass = $oldTaskTypeClass;
269
+                    $this->oldTaskType = \OCP\Server::get($oldTaskTypeClass);
270
+                    $this->l = \OCP\Server::get(IFactory::class)->get('core');
271
+                }
272
+
273
+                public function getId(): string {
274
+                    return Manager::LEGACY_PREFIX_TEXTPROCESSING . $this->oldTaskTypeClass;
275
+                }
276
+
277
+                public function getName(): string {
278
+                    return $this->oldTaskType->getName();
279
+                }
280
+
281
+                public function getDescription(): string {
282
+                    return $this->oldTaskType->getDescription();
283
+                }
284
+
285
+                public function getInputShape(): array {
286
+                    return ['input' => new ShapeDescriptor($this->l->t('Input text'), $this->l->t('The input text'), EShapeType::Text)];
287
+                }
288
+
289
+                public function getOutputShape(): array {
290
+                    return ['output' => new ShapeDescriptor($this->l->t('Input text'), $this->l->t('The input text'), EShapeType::Text)];
291
+                }
292
+            };
293
+            $newTaskTypes[$taskType->getId()] = $taskType;
294
+        }
295
+
296
+        return $newTaskTypes;
297
+    }
298
+
299
+    /**
300
+     * @return IProvider[]
301
+     */
302
+    private function _getTextToImageProviders(): array {
303
+        $oldProviders = $this->textToImageManager->getProviders();
304
+        $newProviders = [];
305
+        foreach ($oldProviders as $oldProvider) {
306
+            $newProvider = new class($oldProvider, $this->appData) implements IProvider, ISynchronousProvider {
307
+                private \OCP\TextToImage\IProvider $provider;
308
+                private IAppData $appData;
309
+
310
+                public function __construct(\OCP\TextToImage\IProvider $provider, IAppData $appData) {
311
+                    $this->provider = $provider;
312
+                    $this->appData = $appData;
313
+                }
314
+
315
+                public function getId(): string {
316
+                    return Manager::LEGACY_PREFIX_TEXTTOIMAGE . $this->provider->getId();
317
+                }
318
+
319
+                public function getName(): string {
320
+                    return $this->provider->getName();
321
+                }
322
+
323
+                public function getTaskTypeId(): string {
324
+                    return TextToImage::ID;
325
+                }
326
+
327
+                public function getExpectedRuntime(): int {
328
+                    return $this->provider->getExpectedRuntime();
329
+                }
330
+
331
+                public function getOptionalInputShape(): array {
332
+                    return [];
333
+                }
334
+
335
+                public function getOptionalOutputShape(): array {
336
+                    return [];
337
+                }
338
+
339
+                public function process(?string $userId, array $input, callable $reportProgress): array {
340
+                    try {
341
+                        $folder = $this->appData->getFolder('text2image');
342
+                    } catch (\OCP\Files\NotFoundException) {
343
+                        $folder = $this->appData->newFolder('text2image');
344
+                    }
345
+                    $resources = [];
346
+                    $files = [];
347
+                    for ($i = 0; $i < $input['numberOfImages']; $i++) {
348
+                        $file = $folder->newFile(time() . '-' . rand(1, 100000) . '-' . $i);
349
+                        $files[] = $file;
350
+                        $resource = $file->write();
351
+                        if ($resource !== false && $resource !== true && is_resource($resource)) {
352
+                            $resources[] = $resource;
353
+                        } else {
354
+                            throw new ProcessingException('Text2Image generation using provider "' . $this->getName() . '" failed: Couldn\'t open file to write.');
355
+                        }
356
+                    }
357
+                    if ($this->provider instanceof \OCP\TextToImage\IProviderWithUserId) {
358
+                        $this->provider->setUserId($userId);
359
+                    }
360
+                    try {
361
+                        $this->provider->generate($input['input'], $resources);
362
+                    } catch (\RuntimeException $e) {
363
+                        throw new ProcessingException($e->getMessage(), 0, $e);
364
+                    }
365
+                    for ($i = 0; $i < $input['numberOfImages']; $i++) {
366
+                        if (is_resource($resources[$i])) {
367
+                            // If $resource hasn't been closed yet, we'll do that here
368
+                            fclose($resources[$i]);
369
+                        }
370
+                    }
371
+                    return ['images' => array_map(fn (ISimpleFile $file) => $file->getContent(), $files)];
372
+                }
373
+
374
+                public function getInputShapeEnumValues(): array {
375
+                    return [];
376
+                }
377
+
378
+                public function getInputShapeDefaults(): array {
379
+                    return [];
380
+                }
381
+
382
+                public function getOptionalInputShapeEnumValues(): array {
383
+                    return [];
384
+                }
385
+
386
+                public function getOptionalInputShapeDefaults(): array {
387
+                    return [];
388
+                }
389
+
390
+                public function getOutputShapeEnumValues(): array {
391
+                    return [];
392
+                }
393
+
394
+                public function getOptionalOutputShapeEnumValues(): array {
395
+                    return [];
396
+                }
397
+            };
398
+            $newProviders[$newProvider->getId()] = $newProvider;
399
+        }
400
+
401
+        return $newProviders;
402
+    }
403
+
404
+    /**
405
+     * This is almost a copy of SpeechToTextManager->getProviders
406
+     * to avoid a dependency cycle between SpeechToTextManager and TaskProcessingManager
407
+     */
408
+    private function _getRawSpeechToTextProviders(): array {
409
+        $context = $this->coordinator->getRegistrationContext();
410
+        if ($context === null) {
411
+            return [];
412
+        }
413
+        $providers = [];
414
+        foreach ($context->getSpeechToTextProviders() as $providerServiceRegistration) {
415
+            $class = $providerServiceRegistration->getService();
416
+            try {
417
+                $providers[$class] = $this->serverContainer->get($class);
418
+            } catch (NotFoundExceptionInterface|ContainerExceptionInterface|\Throwable $e) {
419
+                $this->logger->error('Failed to load SpeechToText provider ' . $class, [
420
+                    'exception' => $e,
421
+                ]);
422
+            }
423
+        }
424
+
425
+        return $providers;
426
+    }
427
+
428
+    /**
429
+     * @return IProvider[]
430
+     */
431
+    private function _getSpeechToTextProviders(): array {
432
+        $oldProviders = $this->_getRawSpeechToTextProviders();
433
+        $newProviders = [];
434
+        foreach ($oldProviders as $oldProvider) {
435
+            $newProvider = new class($oldProvider, $this->rootFolder, $this->appData) implements IProvider, ISynchronousProvider {
436
+                private ISpeechToTextProvider $provider;
437
+                private IAppData $appData;
438
+
439
+                private IRootFolder $rootFolder;
440
+
441
+                public function __construct(ISpeechToTextProvider $provider, IRootFolder $rootFolder, IAppData $appData) {
442
+                    $this->provider = $provider;
443
+                    $this->rootFolder = $rootFolder;
444
+                    $this->appData = $appData;
445
+                }
446
+
447
+                public function getId(): string {
448
+                    if ($this->provider instanceof ISpeechToTextProviderWithId) {
449
+                        return Manager::LEGACY_PREFIX_SPEECHTOTEXT . $this->provider->getId();
450
+                    }
451
+                    return Manager::LEGACY_PREFIX_SPEECHTOTEXT . $this->provider::class;
452
+                }
453
+
454
+                public function getName(): string {
455
+                    return $this->provider->getName();
456
+                }
457
+
458
+                public function getTaskTypeId(): string {
459
+                    return AudioToText::ID;
460
+                }
461
+
462
+                public function getExpectedRuntime(): int {
463
+                    return 60;
464
+                }
465
+
466
+                public function getOptionalInputShape(): array {
467
+                    return [];
468
+                }
469
+
470
+                public function getOptionalOutputShape(): array {
471
+                    return [];
472
+                }
473
+
474
+                public function process(?string $userId, array $input, callable $reportProgress): array {
475
+                    if ($this->provider instanceof \OCP\SpeechToText\ISpeechToTextProviderWithUserId) {
476
+                        $this->provider->setUserId($userId);
477
+                    }
478
+                    try {
479
+                        $result = $this->provider->transcribeFile($input['input']);
480
+                    } catch (\RuntimeException $e) {
481
+                        throw new ProcessingException($e->getMessage(), 0, $e);
482
+                    }
483
+                    return ['output' => $result];
484
+                }
485
+
486
+                public function getInputShapeEnumValues(): array {
487
+                    return [];
488
+                }
489
+
490
+                public function getInputShapeDefaults(): array {
491
+                    return [];
492
+                }
493
+
494
+                public function getOptionalInputShapeEnumValues(): array {
495
+                    return [];
496
+                }
497
+
498
+                public function getOptionalInputShapeDefaults(): array {
499
+                    return [];
500
+                }
501
+
502
+                public function getOutputShapeEnumValues(): array {
503
+                    return [];
504
+                }
505
+
506
+                public function getOptionalOutputShapeEnumValues(): array {
507
+                    return [];
508
+                }
509
+            };
510
+            $newProviders[$newProvider->getId()] = $newProvider;
511
+        }
512
+
513
+        return $newProviders;
514
+    }
515
+
516
+    /**
517
+     * Dispatches the event to collect external providers and task types.
518
+     * Caches the result within the request.
519
+     */
520
+    private function dispatchGetProvidersEvent(): GetTaskProcessingProvidersEvent {
521
+        if ($this->eventResult !== null) {
522
+            return $this->eventResult;
523
+        }
524
+
525
+        $this->eventResult = new GetTaskProcessingProvidersEvent();
526
+        $this->dispatcher->dispatchTyped($this->eventResult);
527
+        return $this->eventResult ;
528
+    }
529
+
530
+    /**
531
+     * @return IProvider[]
532
+     */
533
+    private function _getProviders(): array {
534
+        $context = $this->coordinator->getRegistrationContext();
535
+
536
+        if ($context === null) {
537
+            return [];
538
+        }
539
+
540
+        $providers = [];
541
+
542
+        foreach ($context->getTaskProcessingProviders() as $providerServiceRegistration) {
543
+            $class = $providerServiceRegistration->getService();
544
+            try {
545
+                /** @var IProvider $provider */
546
+                $provider = $this->serverContainer->get($class);
547
+                if (isset($providers[$provider->getId()])) {
548
+                    $this->logger->warning('Task processing provider ' . $class . ' is using ID ' . $provider->getId() . ' which is already used by ' . $providers[$provider->getId()]::class);
549
+                }
550
+                $providers[$provider->getId()] = $provider;
551
+            } catch (\Throwable $e) {
552
+                $this->logger->error('Failed to load task processing provider ' . $class, [
553
+                    'exception' => $e,
554
+                ]);
555
+            }
556
+        }
557
+
558
+        $event = $this->dispatchGetProvidersEvent();
559
+        $externalProviders = $event->getProviders();
560
+        foreach ($externalProviders as $provider) {
561
+            if (!isset($providers[$provider->getId()])) {
562
+                $providers[$provider->getId()] = $provider;
563
+            } else {
564
+                $this->logger->info('Skipping external task processing provider with ID ' . $provider->getId() . ' because a local provider with the same ID already exists.');
565
+            }
566
+        }
567
+
568
+        $providers += $this->_getTextProcessingProviders() + $this->_getTextToImageProviders() + $this->_getSpeechToTextProviders();
569
+
570
+        return $providers;
571
+    }
572
+
573
+    /**
574
+     * @return ITaskType[]
575
+     */
576
+    private function _getTaskTypes(): array {
577
+        $context = $this->coordinator->getRegistrationContext();
578
+
579
+        if ($context === null) {
580
+            return [];
581
+        }
582
+
583
+        if ($this->taskTypes !== null) {
584
+            return $this->taskTypes;
585
+        }
586
+
587
+        // Default task types
588
+        $taskTypes = [
589
+            \OCP\TaskProcessing\TaskTypes\TextToText::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToText::class),
590
+            \OCP\TaskProcessing\TaskTypes\TextToTextTopics::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextTopics::class),
591
+            \OCP\TaskProcessing\TaskTypes\TextToTextHeadline::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextHeadline::class),
592
+            \OCP\TaskProcessing\TaskTypes\TextToTextSummary::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextSummary::class),
593
+            \OCP\TaskProcessing\TaskTypes\TextToTextFormalization::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextFormalization::class),
594
+            \OCP\TaskProcessing\TaskTypes\TextToTextSimplification::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextSimplification::class),
595
+            \OCP\TaskProcessing\TaskTypes\TextToTextChat::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextChat::class),
596
+            \OCP\TaskProcessing\TaskTypes\TextToTextTranslate::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextTranslate::class),
597
+            \OCP\TaskProcessing\TaskTypes\TextToTextReformulation::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextReformulation::class),
598
+            \OCP\TaskProcessing\TaskTypes\TextToImage::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToImage::class),
599
+            \OCP\TaskProcessing\TaskTypes\AudioToText::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\AudioToText::class),
600
+            \OCP\TaskProcessing\TaskTypes\ContextWrite::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\ContextWrite::class),
601
+            \OCP\TaskProcessing\TaskTypes\GenerateEmoji::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\GenerateEmoji::class),
602
+            \OCP\TaskProcessing\TaskTypes\TextToTextChangeTone::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextChangeTone::class),
603
+            \OCP\TaskProcessing\TaskTypes\TextToTextChatWithTools::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextChatWithTools::class),
604
+            \OCP\TaskProcessing\TaskTypes\ContextAgentInteraction::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\ContextAgentInteraction::class),
605
+            \OCP\TaskProcessing\TaskTypes\TextToTextProofread::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextProofread::class),
606
+            \OCP\TaskProcessing\TaskTypes\TextToSpeech::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToSpeech::class),
607
+            \OCP\TaskProcessing\TaskTypes\AudioToAudioChat::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\AudioToAudioChat::class),
608
+            \OCP\TaskProcessing\TaskTypes\ContextAgentAudioInteraction::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\ContextAgentAudioInteraction::class),
609
+            \OCP\TaskProcessing\TaskTypes\AnalyzeImages::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\AnalyzeImages::class),
610
+        ];
611
+
612
+        foreach ($context->getTaskProcessingTaskTypes() as $providerServiceRegistration) {
613
+            $class = $providerServiceRegistration->getService();
614
+            try {
615
+                /** @var ITaskType $provider */
616
+                $taskType = $this->serverContainer->get($class);
617
+                if (isset($taskTypes[$taskType->getId()])) {
618
+                    $this->logger->warning('Task processing task type ' . $class . ' is using ID ' . $taskType->getId() . ' which is already used by ' . $taskTypes[$taskType->getId()]::class);
619
+                }
620
+                $taskTypes[$taskType->getId()] = $taskType;
621
+            } catch (\Throwable $e) {
622
+                $this->logger->error('Failed to load task processing task type ' . $class, [
623
+                    'exception' => $e,
624
+                ]);
625
+            }
626
+        }
627
+
628
+        $event = $this->dispatchGetProvidersEvent();
629
+        $externalTaskTypes = $event->getTaskTypes();
630
+        foreach ($externalTaskTypes as $taskType) {
631
+            if (isset($taskTypes[$taskType->getId()])) {
632
+                $this->logger->warning('External task processing task type is using ID ' . $taskType->getId() . ' which is already used by a locally registered task type (' . get_class($taskTypes[$taskType->getId()]) . ')');
633
+            }
634
+            $taskTypes[$taskType->getId()] = $taskType;
635
+        }
636
+
637
+        $taskTypes += $this->_getTextProcessingTaskTypes();
638
+
639
+        $this->taskTypes = $taskTypes;
640
+        return $this->taskTypes;
641
+    }
642
+
643
+    /**
644
+     * @return array
645
+     */
646
+    private function _getTaskTypeSettings(): array {
647
+        try {
648
+            $json = $this->appConfig->getValueString('core', 'ai.taskprocessing_type_preferences', '', lazy: true);
649
+            if ($json === '') {
650
+                return [];
651
+            }
652
+            return json_decode($json, true, flags: JSON_THROW_ON_ERROR);
653
+        } catch (\JsonException $e) {
654
+            $this->logger->error('Failed to get settings. JSON Error in ai.taskprocessing_type_preferences', ['exception' => $e]);
655
+            $taskTypeSettings = [];
656
+            $taskTypes = $this->_getTaskTypes();
657
+            foreach ($taskTypes as $taskType) {
658
+                $taskTypeSettings[$taskType->getId()] = false;
659
+            };
660
+
661
+            return $taskTypeSettings;
662
+        }
663
+
664
+    }
665
+
666
+    /**
667
+     * @param ShapeDescriptor[] $spec
668
+     * @param array<array-key, string|numeric> $defaults
669
+     * @param array<array-key, ShapeEnumValue[]> $enumValues
670
+     * @param array $io
671
+     * @param bool $optional
672
+     * @return void
673
+     * @throws ValidationException
674
+     */
675
+    private static function validateInput(array $spec, array $defaults, array $enumValues, array $io, bool $optional = false): void {
676
+        foreach ($spec as $key => $descriptor) {
677
+            $type = $descriptor->getShapeType();
678
+            if (!isset($io[$key])) {
679
+                if ($optional) {
680
+                    continue;
681
+                }
682
+                if (isset($defaults[$key])) {
683
+                    if (EShapeType::getScalarType($type) !== $type) {
684
+                        throw new ValidationException('Provider tried to set a default value for a non-scalar slot');
685
+                    }
686
+                    if (EShapeType::isFileType($type)) {
687
+                        throw new ValidationException('Provider tried to set a default value for a slot that is not text or number');
688
+                    }
689
+                    $type->validateInput($defaults[$key]);
690
+                    continue;
691
+                }
692
+                throw new ValidationException('Missing key: "' . $key . '"');
693
+            }
694
+            try {
695
+                $type->validateInput($io[$key]);
696
+                if ($type === EShapeType::Enum) {
697
+                    if (!isset($enumValues[$key])) {
698
+                        throw new ValidationException('Provider did not provide enum values for an enum slot: "' . $key . '"');
699
+                    }
700
+                    $type->validateEnum($io[$key], $enumValues[$key]);
701
+                }
702
+            } catch (ValidationException $e) {
703
+                throw new ValidationException('Failed to validate input key "' . $key . '": ' . $e->getMessage());
704
+            }
705
+        }
706
+    }
707
+
708
+    /**
709
+     * Takes task input data and replaces fileIds with File objects
710
+     *
711
+     * @param array<array-key, list<numeric|string>|numeric|string> $input
712
+     * @param array<array-key, numeric|string> ...$defaultSpecs the specs
713
+     * @return array<array-key, list<numeric|string>|numeric|string>
714
+     */
715
+    public function fillInputDefaults(array $input, ...$defaultSpecs): array {
716
+        $spec = array_reduce($defaultSpecs, fn ($carry, $spec) => array_merge($carry, $spec), []);
717
+        return array_merge($spec, $input);
718
+    }
719
+
720
+    /**
721
+     * @param ShapeDescriptor[] $spec
722
+     * @param array<array-key, ShapeEnumValue[]> $enumValues
723
+     * @param array $io
724
+     * @param bool $optional
725
+     * @return void
726
+     * @throws ValidationException
727
+     */
728
+    private static function validateOutputWithFileIds(array $spec, array $enumValues, array $io, bool $optional = false): void {
729
+        foreach ($spec as $key => $descriptor) {
730
+            $type = $descriptor->getShapeType();
731
+            if (!isset($io[$key])) {
732
+                if ($optional) {
733
+                    continue;
734
+                }
735
+                throw new ValidationException('Missing key: "' . $key . '"');
736
+            }
737
+            try {
738
+                $type->validateOutputWithFileIds($io[$key]);
739
+                if (isset($enumValues[$key])) {
740
+                    $type->validateEnum($io[$key], $enumValues[$key]);
741
+                }
742
+            } catch (ValidationException $e) {
743
+                throw new ValidationException('Failed to validate output key "' . $key . '": ' . $e->getMessage());
744
+            }
745
+        }
746
+    }
747
+
748
+    /**
749
+     * @param ShapeDescriptor[] $spec
750
+     * @param array<array-key, ShapeEnumValue[]> $enumValues
751
+     * @param array $io
752
+     * @param bool $optional
753
+     * @return void
754
+     * @throws ValidationException
755
+     */
756
+    private static function validateOutputWithFileData(array $spec, array $enumValues, array $io, bool $optional = false): void {
757
+        foreach ($spec as $key => $descriptor) {
758
+            $type = $descriptor->getShapeType();
759
+            if (!isset($io[$key])) {
760
+                if ($optional) {
761
+                    continue;
762
+                }
763
+                throw new ValidationException('Missing key: "' . $key . '"');
764
+            }
765
+            try {
766
+                $type->validateOutputWithFileData($io[$key]);
767
+                if (isset($enumValues[$key])) {
768
+                    $type->validateEnum($io[$key], $enumValues[$key]);
769
+                }
770
+            } catch (ValidationException $e) {
771
+                throw new ValidationException('Failed to validate output key "' . $key . '": ' . $e->getMessage());
772
+            }
773
+        }
774
+    }
775
+
776
+    /**
777
+     * @param array<array-key, T> $array The array to filter
778
+     * @param ShapeDescriptor[] ...$specs the specs that define which keys to keep
779
+     * @return array<array-key, T>
780
+     * @psalm-template T
781
+     */
782
+    private function removeSuperfluousArrayKeys(array $array, ...$specs): array {
783
+        $keys = array_unique(array_reduce($specs, fn ($carry, $spec) => array_merge($carry, array_keys($spec)), []));
784
+        $keys = array_filter($keys, fn ($key) => array_key_exists($key, $array));
785
+        $values = array_map(fn (string $key) => $array[$key], $keys);
786
+        return array_combine($keys, $values);
787
+    }
788
+
789
+    public function hasProviders(): bool {
790
+        return count($this->getProviders()) !== 0;
791
+    }
792
+
793
+    public function getProviders(): array {
794
+        if ($this->providers === null) {
795
+            $this->providers = $this->_getProviders();
796
+        }
797
+
798
+        return $this->providers;
799
+    }
800
+
801
+    public function getPreferredProvider(string $taskTypeId) {
802
+        try {
803
+            if ($this->preferences === null) {
804
+                $this->preferences = $this->distributedCache->get('ai.taskprocessing_provider_preferences');
805
+                if ($this->preferences === null) {
806
+                    $this->preferences = json_decode(
807
+                        $this->appConfig->getValueString('core', 'ai.taskprocessing_provider_preferences', 'null', lazy: true),
808
+                        associative: true,
809
+                        flags: JSON_THROW_ON_ERROR,
810
+                    );
811
+                    $this->distributedCache->set('ai.taskprocessing_provider_preferences', $this->preferences, 60 * 3);
812
+                }
813
+            }
814
+
815
+            $providers = $this->getProviders();
816
+            if (isset($this->preferences[$taskTypeId])) {
817
+                $providersById = $this->providersById ?? array_reduce($providers, static function (array $carry, IProvider $provider) {
818
+                    $carry[$provider->getId()] = $provider;
819
+                    return $carry;
820
+                }, []);
821
+                $this->providersById = $providersById;
822
+                if (isset($providersById[$this->preferences[$taskTypeId]])) {
823
+                    return $providersById[$this->preferences[$taskTypeId]];
824
+                }
825
+            }
826
+            // By default, use the first available provider
827
+            foreach ($providers as $provider) {
828
+                if ($provider->getTaskTypeId() === $taskTypeId) {
829
+                    return $provider;
830
+                }
831
+            }
832
+        } catch (\JsonException $e) {
833
+            $this->logger->warning('Failed to parse provider preferences while getting preferred provider for task type ' . $taskTypeId, ['exception' => $e]);
834
+        }
835
+        throw new \OCP\TaskProcessing\Exception\Exception('No matching provider found');
836
+    }
837
+
838
+    public function getAvailableTaskTypes(bool $showDisabled = false, ?string $userId = null): array {
839
+        // We cache by language, because some task type fields are translated
840
+        $cacheKey = self::TASK_TYPES_CACHE_KEY . ':' . $this->l10nFactory->findLanguage();
841
+
842
+        // userId will be obtained from the session if left to null
843
+        if (!$this->checkGuestAccess($userId)) {
844
+            return [];
845
+        }
846
+        if ($this->availableTaskTypes === null) {
847
+            $cachedValue = $this->distributedCache->get($cacheKey);
848
+            if ($cachedValue !== null) {
849
+                $this->availableTaskTypes = unserialize($cachedValue);
850
+            }
851
+        }
852
+        // Either we have no cache or showDisabled is turned on, which we don't want to cache, ever.
853
+        if ($this->availableTaskTypes === null || $showDisabled) {
854
+            $taskTypes = $this->_getTaskTypes();
855
+            $taskTypeSettings = $this->_getTaskTypeSettings();
856
+
857
+            $availableTaskTypes = [];
858
+            foreach ($taskTypes as $taskType) {
859
+                if ((!$showDisabled) && isset($taskTypeSettings[$taskType->getId()]) && !$taskTypeSettings[$taskType->getId()]) {
860
+                    continue;
861
+                }
862
+                try {
863
+                    $provider = $this->getPreferredProvider($taskType->getId());
864
+                } catch (\OCP\TaskProcessing\Exception\Exception $e) {
865
+                    continue;
866
+                }
867
+                try {
868
+                    $availableTaskTypes[$provider->getTaskTypeId()] = [
869
+                        'name' => $taskType->getName(),
870
+                        'description' => $taskType->getDescription(),
871
+                        'optionalInputShape' => $provider->getOptionalInputShape(),
872
+                        'inputShapeEnumValues' => $provider->getInputShapeEnumValues(),
873
+                        'inputShapeDefaults' => $provider->getInputShapeDefaults(),
874
+                        'inputShape' => $taskType->getInputShape(),
875
+                        'optionalInputShapeEnumValues' => $provider->getOptionalInputShapeEnumValues(),
876
+                        'optionalInputShapeDefaults' => $provider->getOptionalInputShapeDefaults(),
877
+                        'outputShape' => $taskType->getOutputShape(),
878
+                        'outputShapeEnumValues' => $provider->getOutputShapeEnumValues(),
879
+                        'optionalOutputShape' => $provider->getOptionalOutputShape(),
880
+                        'optionalOutputShapeEnumValues' => $provider->getOptionalOutputShapeEnumValues(),
881
+                    ];
882
+                } catch (\Throwable $e) {
883
+                    $this->logger->error('Failed to set up TaskProcessing provider ' . $provider::class, ['exception' => $e]);
884
+                }
885
+            }
886
+
887
+            if ($showDisabled) {
888
+                // Do not cache showDisabled, ever.
889
+                return $availableTaskTypes;
890
+            }
891
+
892
+            $this->availableTaskTypes = $availableTaskTypes;
893
+            $this->distributedCache->set($cacheKey, serialize($this->availableTaskTypes), 60);
894
+        }
895
+
896
+
897
+        return $this->availableTaskTypes;
898
+    }
899
+    public function getAvailableTaskTypeIds(bool $showDisabled = false, ?string $userId = null): array {
900
+        // userId will be obtained from the session if left to null
901
+        if (!$this->checkGuestAccess($userId)) {
902
+            return [];
903
+        }
904
+        if ($this->availableTaskTypeIds === null) {
905
+            $cachedValue = $this->distributedCache->get(self::TASK_TYPE_IDS_CACHE_KEY);
906
+            if ($cachedValue !== null) {
907
+                $this->availableTaskTypeIds = $cachedValue;
908
+            }
909
+        }
910
+        // Either we have no cache or showDisabled is turned on, which we don't want to cache, ever.
911
+        if ($this->availableTaskTypeIds === null || $showDisabled) {
912
+            $taskTypes = $this->_getTaskTypes();
913
+            $taskTypeSettings = $this->_getTaskTypeSettings();
914
+
915
+            $availableTaskTypeIds = [];
916
+            foreach ($taskTypes as $taskType) {
917
+                if ((!$showDisabled) && isset($taskTypeSettings[$taskType->getId()]) && !$taskTypeSettings[$taskType->getId()]) {
918
+                    continue;
919
+                }
920
+                try {
921
+                    $provider = $this->getPreferredProvider($taskType->getId());
922
+                } catch (\OCP\TaskProcessing\Exception\Exception $e) {
923
+                    continue;
924
+                }
925
+                $availableTaskTypeIds[] = $taskType->getId();
926
+            }
927
+
928
+            if ($showDisabled) {
929
+                // Do not cache showDisabled, ever.
930
+                return $availableTaskTypeIds;
931
+            }
932
+
933
+            $this->availableTaskTypeIds = $availableTaskTypeIds;
934
+            $this->distributedCache->set(self::TASK_TYPE_IDS_CACHE_KEY, $this->availableTaskTypeIds, 60);
935
+        }
936
+
937
+
938
+        return $this->availableTaskTypeIds;
939
+    }
940
+
941
+    public function canHandleTask(Task $task): bool {
942
+        return isset($this->getAvailableTaskTypes()[$task->getTaskTypeId()]);
943
+    }
944
+
945
+    private function checkGuestAccess(?string $userId = null): bool {
946
+        if ($userId === null && !$this->userSession->isLoggedIn()) {
947
+            return true;
948
+        }
949
+        if ($userId === null) {
950
+            $user = $this->userSession->getUser();
951
+        } else {
952
+            $user = $this->userManager->get($userId);
953
+        }
954
+
955
+        $guestsAllowed = $this->appConfig->getValueString('core', 'ai.taskprocessing_guests', 'false');
956
+        if ($guestsAllowed == 'true' || !class_exists(\OCA\Guests\UserBackend::class) || !($user->getBackend() instanceof \OCA\Guests\UserBackend)) {
957
+            return true;
958
+        }
959
+        return false;
960
+    }
961
+
962
+    public function scheduleTask(Task $task): void {
963
+        if (!$this->checkGuestAccess($task->getUserId())) {
964
+            throw new \OCP\TaskProcessing\Exception\PreConditionNotMetException('Access to this resource is forbidden for guests.');
965
+        }
966
+        if (!$this->canHandleTask($task)) {
967
+            throw new \OCP\TaskProcessing\Exception\PreConditionNotMetException('No task processing provider is installed that can handle this task type: ' . $task->getTaskTypeId());
968
+        }
969
+        $this->prepareTask($task);
970
+        $task->setStatus(Task::STATUS_SCHEDULED);
971
+        $this->storeTask($task);
972
+        // schedule synchronous job if the provider is synchronous
973
+        $provider = $this->getPreferredProvider($task->getTaskTypeId());
974
+        if ($provider instanceof ISynchronousProvider) {
975
+            $this->jobList->add(SynchronousBackgroundJob::class, null);
976
+        }
977
+    }
978
+
979
+    public function runTask(Task $task): Task {
980
+        if (!$this->checkGuestAccess($task->getUserId())) {
981
+            throw new \OCP\TaskProcessing\Exception\PreConditionNotMetException('Access to this resource is forbidden for guests.');
982
+        }
983
+        if (!$this->canHandleTask($task)) {
984
+            throw new \OCP\TaskProcessing\Exception\PreConditionNotMetException('No task processing provider is installed that can handle this task type: ' . $task->getTaskTypeId());
985
+        }
986
+
987
+        $provider = $this->getPreferredProvider($task->getTaskTypeId());
988
+        if ($provider instanceof ISynchronousProvider) {
989
+            $this->prepareTask($task);
990
+            $task->setStatus(Task::STATUS_SCHEDULED);
991
+            $this->storeTask($task);
992
+            $this->processTask($task, $provider);
993
+            $task = $this->getTask($task->getId());
994
+        } else {
995
+            $this->scheduleTask($task);
996
+            // poll task
997
+            while ($task->getStatus() === Task::STATUS_SCHEDULED || $task->getStatus() === Task::STATUS_RUNNING) {
998
+                sleep(1);
999
+                $task = $this->getTask($task->getId());
1000
+            }
1001
+        }
1002
+        return $task;
1003
+    }
1004
+
1005
+    public function processTask(Task $task, ISynchronousProvider $provider): bool {
1006
+        try {
1007
+            try {
1008
+                $input = $this->prepareInputData($task);
1009
+            } catch (GenericFileException|NotPermittedException|LockedException|ValidationException|UnauthorizedException $e) {
1010
+                $this->logger->warning('Failed to prepare input data for a TaskProcessing task with synchronous provider ' . $provider->getId(), ['exception' => $e]);
1011
+                $this->setTaskResult($task->getId(), $e->getMessage(), null);
1012
+                return false;
1013
+            }
1014
+            try {
1015
+                $this->setTaskStatus($task, Task::STATUS_RUNNING);
1016
+                $output = $provider->process($task->getUserId(), $input, fn (float $progress) => $this->setTaskProgress($task->getId(), $progress));
1017
+            } catch (ProcessingException $e) {
1018
+                $this->logger->warning('Failed to process a TaskProcessing task with synchronous provider ' . $provider->getId(), ['exception' => $e]);
1019
+                $this->setTaskResult($task->getId(), $e->getMessage(), null);
1020
+                return false;
1021
+            } catch (\Throwable $e) {
1022
+                $this->logger->error('Unknown error while processing TaskProcessing task', ['exception' => $e]);
1023
+                $this->setTaskResult($task->getId(), $e->getMessage(), null);
1024
+                return false;
1025
+            }
1026
+            $this->setTaskResult($task->getId(), null, $output);
1027
+        } catch (NotFoundException $e) {
1028
+            $this->logger->info('Could not find task anymore after execution. Moving on.', ['exception' => $e]);
1029
+        } catch (Exception $e) {
1030
+            $this->logger->error('Failed to report result of TaskProcessing task', ['exception' => $e]);
1031
+        }
1032
+        return true;
1033
+    }
1034
+
1035
+    public function deleteTask(Task $task): void {
1036
+        $taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
1037
+        $this->taskMapper->delete($taskEntity);
1038
+    }
1039
+
1040
+    public function getTask(int $id): Task {
1041
+        try {
1042
+            $taskEntity = $this->taskMapper->find($id);
1043
+            return $taskEntity->toPublicTask();
1044
+        } catch (DoesNotExistException $e) {
1045
+            throw new NotFoundException('Couldn\'t find task with id ' . $id, 0, $e);
1046
+        } catch (MultipleObjectsReturnedException|\OCP\DB\Exception $e) {
1047
+            throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding the task', 0, $e);
1048
+        } catch (\JsonException $e) {
1049
+            throw new \OCP\TaskProcessing\Exception\Exception('There was a problem parsing JSON after finding the task', 0, $e);
1050
+        }
1051
+    }
1052
+
1053
+    public function cancelTask(int $id): void {
1054
+        $task = $this->getTask($id);
1055
+        if ($task->getStatus() !== Task::STATUS_SCHEDULED && $task->getStatus() !== Task::STATUS_RUNNING) {
1056
+            return;
1057
+        }
1058
+        $task->setStatus(Task::STATUS_CANCELLED);
1059
+        $task->setEndedAt(time());
1060
+        $taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
1061
+        try {
1062
+            $this->taskMapper->update($taskEntity);
1063
+            $this->runWebhook($task);
1064
+        } catch (\OCP\DB\Exception $e) {
1065
+            throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding the task', 0, $e);
1066
+        }
1067
+    }
1068
+
1069
+    public function setTaskProgress(int $id, float $progress): bool {
1070
+        // TODO: Not sure if we should rather catch the exceptions of getTask here and fail silently
1071
+        $task = $this->getTask($id);
1072
+        if ($task->getStatus() === Task::STATUS_CANCELLED) {
1073
+            return false;
1074
+        }
1075
+        // only set the start time if the task is going from scheduled to running
1076
+        if ($task->getstatus() === Task::STATUS_SCHEDULED) {
1077
+            $task->setStartedAt(time());
1078
+        }
1079
+        $task->setStatus(Task::STATUS_RUNNING);
1080
+        $task->setProgress($progress);
1081
+        $taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
1082
+        try {
1083
+            $this->taskMapper->update($taskEntity);
1084
+        } catch (\OCP\DB\Exception $e) {
1085
+            throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding the task', 0, $e);
1086
+        }
1087
+        return true;
1088
+    }
1089
+
1090
+    public function setTaskResult(int $id, ?string $error, ?array $result, bool $isUsingFileIds = false): void {
1091
+        // TODO: Not sure if we should rather catch the exceptions of getTask here and fail silently
1092
+        $task = $this->getTask($id);
1093
+        if ($task->getStatus() === Task::STATUS_CANCELLED) {
1094
+            $this->logger->info('A TaskProcessing ' . $task->getTaskTypeId() . ' task with id ' . $id . ' finished but was cancelled in the mean time. Moving on without storing result.');
1095
+            return;
1096
+        }
1097
+        if ($error !== null) {
1098
+            $task->setStatus(Task::STATUS_FAILED);
1099
+            $task->setEndedAt(time());
1100
+            // truncate error message to 1000 characters
1101
+            $task->setErrorMessage(mb_substr($error, 0, 1000));
1102
+            $this->logger->warning('A TaskProcessing ' . $task->getTaskTypeId() . ' task with id ' . $id . ' failed with the following message: ' . $error);
1103
+        } elseif ($result !== null) {
1104
+            $taskTypes = $this->getAvailableTaskTypes();
1105
+            $outputShape = $taskTypes[$task->getTaskTypeId()]['outputShape'];
1106
+            $outputShapeEnumValues = $taskTypes[$task->getTaskTypeId()]['outputShapeEnumValues'];
1107
+            $optionalOutputShape = $taskTypes[$task->getTaskTypeId()]['optionalOutputShape'];
1108
+            $optionalOutputShapeEnumValues = $taskTypes[$task->getTaskTypeId()]['optionalOutputShapeEnumValues'];
1109
+            try {
1110
+                // validate output
1111
+                if (!$isUsingFileIds) {
1112
+                    $this->validateOutputWithFileData($outputShape, $outputShapeEnumValues, $result);
1113
+                    $this->validateOutputWithFileData($optionalOutputShape, $optionalOutputShapeEnumValues, $result, true);
1114
+                } else {
1115
+                    $this->validateOutputWithFileIds($outputShape, $outputShapeEnumValues, $result);
1116
+                    $this->validateOutputWithFileIds($optionalOutputShape, $optionalOutputShapeEnumValues, $result, true);
1117
+                }
1118
+                $output = $this->removeSuperfluousArrayKeys($result, $outputShape, $optionalOutputShape);
1119
+                // extract raw data and put it in files, replace it with file ids
1120
+                if (!$isUsingFileIds) {
1121
+                    $output = $this->encapsulateOutputFileData($output, $outputShape, $optionalOutputShape);
1122
+                } else {
1123
+                    $this->validateOutputFileIds($output, $outputShape, $optionalOutputShape);
1124
+                }
1125
+                // Turn file objects into IDs
1126
+                foreach ($output as $key => $value) {
1127
+                    if ($value instanceof Node) {
1128
+                        $output[$key] = $value->getId();
1129
+                    }
1130
+                    if (is_array($value) && isset($value[0]) && $value[0] instanceof Node) {
1131
+                        $output[$key] = array_map(fn ($node) => $node->getId(), $value);
1132
+                    }
1133
+                }
1134
+                $task->setOutput($output);
1135
+                $task->setProgress(1);
1136
+                $task->setStatus(Task::STATUS_SUCCESSFUL);
1137
+                $task->setEndedAt(time());
1138
+            } catch (ValidationException $e) {
1139
+                $task->setProgress(1);
1140
+                $task->setStatus(Task::STATUS_FAILED);
1141
+                $task->setEndedAt(time());
1142
+                $error = 'The task was processed successfully but the provider\'s output doesn\'t pass validation against the task type\'s outputShape spec and/or the provider\'s own optionalOutputShape spec';
1143
+                $task->setErrorMessage($error);
1144
+                $this->logger->error($error, ['exception' => $e, 'output' => $result]);
1145
+            } catch (NotPermittedException $e) {
1146
+                $task->setProgress(1);
1147
+                $task->setStatus(Task::STATUS_FAILED);
1148
+                $task->setEndedAt(time());
1149
+                $error = 'The task was processed successfully but storing the output in a file failed';
1150
+                $task->setErrorMessage($error);
1151
+                $this->logger->error($error, ['exception' => $e]);
1152
+            } catch (InvalidPathException|\OCP\Files\NotFoundException $e) {
1153
+                $task->setProgress(1);
1154
+                $task->setStatus(Task::STATUS_FAILED);
1155
+                $task->setEndedAt(time());
1156
+                $error = 'The task was processed successfully but the result file could not be found';
1157
+                $task->setErrorMessage($error);
1158
+                $this->logger->error($error, ['exception' => $e]);
1159
+            }
1160
+        }
1161
+        try {
1162
+            $taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
1163
+        } catch (\JsonException $e) {
1164
+            throw new \OCP\TaskProcessing\Exception\Exception('The task was processed successfully but the provider\'s output could not be encoded as JSON for the database.', 0, $e);
1165
+        }
1166
+        try {
1167
+            $this->taskMapper->update($taskEntity);
1168
+            $this->runWebhook($task);
1169
+        } catch (\OCP\DB\Exception $e) {
1170
+            throw new \OCP\TaskProcessing\Exception\Exception($e->getMessage());
1171
+        }
1172
+        if ($task->getStatus() === Task::STATUS_SUCCESSFUL) {
1173
+            $event = new TaskSuccessfulEvent($task);
1174
+        } else {
1175
+            $event = new TaskFailedEvent($task, $error);
1176
+        }
1177
+        $this->dispatcher->dispatchTyped($event);
1178
+    }
1179
+
1180
+    public function getNextScheduledTask(array $taskTypeIds = [], array $taskIdsToIgnore = []): Task {
1181
+        try {
1182
+            $taskEntity = $this->taskMapper->findOldestScheduledByType($taskTypeIds, $taskIdsToIgnore);
1183
+            return $taskEntity->toPublicTask();
1184
+        } catch (DoesNotExistException $e) {
1185
+            throw new \OCP\TaskProcessing\Exception\NotFoundException('Could not find the task', 0, $e);
1186
+        } catch (\OCP\DB\Exception $e) {
1187
+            throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding the task', 0, $e);
1188
+        } catch (\JsonException $e) {
1189
+            throw new \OCP\TaskProcessing\Exception\Exception('There was a problem parsing JSON after finding the task', 0, $e);
1190
+        }
1191
+    }
1192
+
1193
+    /**
1194
+     * Takes task input data and replaces fileIds with File objects
1195
+     *
1196
+     * @param string|null $userId
1197
+     * @param array<array-key, list<numeric|string>|numeric|string> $input
1198
+     * @param ShapeDescriptor[] ...$specs the specs
1199
+     * @return array<array-key, list<File|numeric|string>|numeric|string|File>
1200
+     * @throws GenericFileException|LockedException|NotPermittedException|ValidationException|UnauthorizedException
1201
+     */
1202
+    public function fillInputFileData(?string $userId, array $input, ...$specs): array {
1203
+        if ($userId !== null) {
1204
+            \OC_Util::setupFS($userId);
1205
+        }
1206
+        $newInputOutput = [];
1207
+        $spec = array_reduce($specs, fn ($carry, $spec) => $carry + $spec, []);
1208
+        foreach ($spec as $key => $descriptor) {
1209
+            $type = $descriptor->getShapeType();
1210
+            if (!isset($input[$key])) {
1211
+                continue;
1212
+            }
1213
+            if (!in_array(EShapeType::getScalarType($type), [EShapeType::Image, EShapeType::Audio, EShapeType::Video, EShapeType::File], true)) {
1214
+                $newInputOutput[$key] = $input[$key];
1215
+                continue;
1216
+            }
1217
+            if (EShapeType::getScalarType($type) === $type) {
1218
+                // is scalar
1219
+                $node = $this->validateFileId((int)$input[$key]);
1220
+                $this->validateUserAccessToFile($input[$key], $userId);
1221
+                $newInputOutput[$key] = $node;
1222
+            } else {
1223
+                // is list
1224
+                $newInputOutput[$key] = [];
1225
+                foreach ($input[$key] as $item) {
1226
+                    $node = $this->validateFileId((int)$item);
1227
+                    $this->validateUserAccessToFile($item, $userId);
1228
+                    $newInputOutput[$key][] = $node;
1229
+                }
1230
+            }
1231
+        }
1232
+        return $newInputOutput;
1233
+    }
1234
+
1235
+    public function getUserTask(int $id, ?string $userId): Task {
1236
+        try {
1237
+            $taskEntity = $this->taskMapper->findByIdAndUser($id, $userId);
1238
+            return $taskEntity->toPublicTask();
1239
+        } catch (DoesNotExistException $e) {
1240
+            throw new \OCP\TaskProcessing\Exception\NotFoundException('Could not find the task', 0, $e);
1241
+        } catch (MultipleObjectsReturnedException|\OCP\DB\Exception $e) {
1242
+            throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding the task', 0, $e);
1243
+        } catch (\JsonException $e) {
1244
+            throw new \OCP\TaskProcessing\Exception\Exception('There was a problem parsing JSON after finding the task', 0, $e);
1245
+        }
1246
+    }
1247
+
1248
+    public function getUserTasks(?string $userId, ?string $taskTypeId = null, ?string $customId = null): array {
1249
+        try {
1250
+            $taskEntities = $this->taskMapper->findByUserAndTaskType($userId, $taskTypeId, $customId);
1251
+            return array_map(fn ($taskEntity): Task => $taskEntity->toPublicTask(), $taskEntities);
1252
+        } catch (\OCP\DB\Exception $e) {
1253
+            throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding the tasks', 0, $e);
1254
+        } catch (\JsonException $e) {
1255
+            throw new \OCP\TaskProcessing\Exception\Exception('There was a problem parsing JSON after finding the tasks', 0, $e);
1256
+        }
1257
+    }
1258
+
1259
+    public function getTasks(
1260
+        ?string $userId, ?string $taskTypeId = null, ?string $appId = null, ?string $customId = null,
1261
+        ?int $status = null, ?int $scheduleAfter = null, ?int $endedBefore = null,
1262
+    ): array {
1263
+        try {
1264
+            $taskEntities = $this->taskMapper->findTasks($userId, $taskTypeId, $appId, $customId, $status, $scheduleAfter, $endedBefore);
1265
+            return array_map(fn ($taskEntity): Task => $taskEntity->toPublicTask(), $taskEntities);
1266
+        } catch (\OCP\DB\Exception $e) {
1267
+            throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding the tasks', 0, $e);
1268
+        } catch (\JsonException $e) {
1269
+            throw new \OCP\TaskProcessing\Exception\Exception('There was a problem parsing JSON after finding the tasks', 0, $e);
1270
+        }
1271
+    }
1272
+
1273
+    public function getUserTasksByApp(?string $userId, string $appId, ?string $customId = null): array {
1274
+        try {
1275
+            $taskEntities = $this->taskMapper->findUserTasksByApp($userId, $appId, $customId);
1276
+            return array_map(fn ($taskEntity): Task => $taskEntity->toPublicTask(), $taskEntities);
1277
+        } catch (\OCP\DB\Exception $e) {
1278
+            throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding a task', 0, $e);
1279
+        } catch (\JsonException $e) {
1280
+            throw new \OCP\TaskProcessing\Exception\Exception('There was a problem parsing JSON after finding a task', 0, $e);
1281
+        }
1282
+    }
1283
+
1284
+    /**
1285
+     *Takes task input or output and replaces base64 data with file ids
1286
+     *
1287
+     * @param array $output
1288
+     * @param ShapeDescriptor[] ...$specs the specs that define which keys to keep
1289
+     * @return array
1290
+     * @throws NotPermittedException
1291
+     */
1292
+    public function encapsulateOutputFileData(array $output, ...$specs): array {
1293
+        $newOutput = [];
1294
+        try {
1295
+            $folder = $this->appData->getFolder('TaskProcessing');
1296
+        } catch (\OCP\Files\NotFoundException) {
1297
+            $folder = $this->appData->newFolder('TaskProcessing');
1298
+        }
1299
+        $spec = array_reduce($specs, fn ($carry, $spec) => $carry + $spec, []);
1300
+        foreach ($spec as $key => $descriptor) {
1301
+            $type = $descriptor->getShapeType();
1302
+            if (!isset($output[$key])) {
1303
+                continue;
1304
+            }
1305
+            if (!in_array(EShapeType::getScalarType($type), [EShapeType::Image, EShapeType::Audio, EShapeType::Video, EShapeType::File], true)) {
1306
+                $newOutput[$key] = $output[$key];
1307
+                continue;
1308
+            }
1309
+            if (EShapeType::getScalarType($type) === $type) {
1310
+                /** @var SimpleFile $file */
1311
+                $file = $folder->newFile(time() . '-' . rand(1, 100000), $output[$key]);
1312
+                $newOutput[$key] = $file->getId(); // polymorphic call to SimpleFile
1313
+            } else {
1314
+                $newOutput = [];
1315
+                foreach ($output[$key] as $item) {
1316
+                    /** @var SimpleFile $file */
1317
+                    $file = $folder->newFile(time() . '-' . rand(1, 100000), $item);
1318
+                    $newOutput[$key][] = $file->getId();
1319
+                }
1320
+            }
1321
+        }
1322
+        return $newOutput;
1323
+    }
1324
+
1325
+    /**
1326
+     * @param Task $task
1327
+     * @return array<array-key, list<numeric|string|File>|numeric|string|File>
1328
+     * @throws GenericFileException
1329
+     * @throws LockedException
1330
+     * @throws NotPermittedException
1331
+     * @throws ValidationException|UnauthorizedException
1332
+     */
1333
+    public function prepareInputData(Task $task): array {
1334
+        $taskTypes = $this->getAvailableTaskTypes();
1335
+        $inputShape = $taskTypes[$task->getTaskTypeId()]['inputShape'];
1336
+        $optionalInputShape = $taskTypes[$task->getTaskTypeId()]['optionalInputShape'];
1337
+        $input = $task->getInput();
1338
+        $input = $this->removeSuperfluousArrayKeys($input, $inputShape, $optionalInputShape);
1339
+        $input = $this->fillInputFileData($task->getUserId(), $input, $inputShape, $optionalInputShape);
1340
+        return $input;
1341
+    }
1342
+
1343
+    public function lockTask(Task $task): bool {
1344
+        $taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
1345
+        if ($this->taskMapper->lockTask($taskEntity) === 0) {
1346
+            return false;
1347
+        }
1348
+        $task->setStatus(Task::STATUS_RUNNING);
1349
+        return true;
1350
+    }
1351
+
1352
+    /**
1353
+     * @throws \JsonException
1354
+     * @throws Exception
1355
+     */
1356
+    public function setTaskStatus(Task $task, int $status): void {
1357
+        $currentTaskStatus = $task->getStatus();
1358
+        if ($currentTaskStatus === Task::STATUS_SCHEDULED && $status === Task::STATUS_RUNNING) {
1359
+            $task->setStartedAt(time());
1360
+        } elseif ($currentTaskStatus === Task::STATUS_RUNNING && ($status === Task::STATUS_FAILED || $status === Task::STATUS_CANCELLED)) {
1361
+            $task->setEndedAt(time());
1362
+        } elseif ($currentTaskStatus === Task::STATUS_UNKNOWN && $status === Task::STATUS_SCHEDULED) {
1363
+            $task->setScheduledAt(time());
1364
+        }
1365
+        $task->setStatus($status);
1366
+        $taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
1367
+        $this->taskMapper->update($taskEntity);
1368
+    }
1369
+
1370
+    /**
1371
+     * Validate input, fill input default values, set completionExpectedAt, set scheduledAt
1372
+     *
1373
+     * @param Task $task
1374
+     * @return void
1375
+     * @throws UnauthorizedException
1376
+     * @throws ValidationException
1377
+     * @throws \OCP\TaskProcessing\Exception\Exception
1378
+     */
1379
+    private function prepareTask(Task $task): void {
1380
+        $taskTypes = $this->getAvailableTaskTypes();
1381
+        $taskType = $taskTypes[$task->getTaskTypeId()];
1382
+        $inputShape = $taskType['inputShape'];
1383
+        $inputShapeDefaults = $taskType['inputShapeDefaults'];
1384
+        $inputShapeEnumValues = $taskType['inputShapeEnumValues'];
1385
+        $optionalInputShape = $taskType['optionalInputShape'];
1386
+        $optionalInputShapeEnumValues = $taskType['optionalInputShapeEnumValues'];
1387
+        $optionalInputShapeDefaults = $taskType['optionalInputShapeDefaults'];
1388
+        // validate input
1389
+        $this->validateInput($inputShape, $inputShapeDefaults, $inputShapeEnumValues, $task->getInput());
1390
+        $this->validateInput($optionalInputShape, $optionalInputShapeDefaults, $optionalInputShapeEnumValues, $task->getInput(), true);
1391
+        // authenticate access to mentioned files
1392
+        $ids = [];
1393
+        foreach ($inputShape + $optionalInputShape as $key => $descriptor) {
1394
+            if (in_array(EShapeType::getScalarType($descriptor->getShapeType()), [EShapeType::File, EShapeType::Image, EShapeType::Audio, EShapeType::Video], true)) {
1395
+                /** @var list<int>|int $inputSlot */
1396
+                $inputSlot = $task->getInput()[$key];
1397
+                if (is_array($inputSlot)) {
1398
+                    $ids += $inputSlot;
1399
+                } else {
1400
+                    $ids[] = $inputSlot;
1401
+                }
1402
+            }
1403
+        }
1404
+        foreach ($ids as $fileId) {
1405
+            $this->validateFileId($fileId);
1406
+            $this->validateUserAccessToFile($fileId, $task->getUserId());
1407
+        }
1408
+        // remove superfluous keys and set input
1409
+        $input = $this->removeSuperfluousArrayKeys($task->getInput(), $inputShape, $optionalInputShape);
1410
+        $inputWithDefaults = $this->fillInputDefaults($input, $inputShapeDefaults, $optionalInputShapeDefaults);
1411
+        $task->setInput($inputWithDefaults);
1412
+        $task->setScheduledAt(time());
1413
+        $provider = $this->getPreferredProvider($task->getTaskTypeId());
1414
+        // calculate expected completion time
1415
+        $completionExpectedAt = new \DateTime('now');
1416
+        $completionExpectedAt->add(new \DateInterval('PT' . $provider->getExpectedRuntime() . 'S'));
1417
+        $task->setCompletionExpectedAt($completionExpectedAt);
1418
+    }
1419
+
1420
+    /**
1421
+     * Store the task in the DB and set its ID in the \OCP\TaskProcessing\Task input param
1422
+     *
1423
+     * @param Task $task
1424
+     * @return void
1425
+     * @throws Exception
1426
+     * @throws \JsonException
1427
+     */
1428
+    private function storeTask(Task $task): void {
1429
+        // create a db entity and insert into db table
1430
+        $taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
1431
+        $this->taskMapper->insert($taskEntity);
1432
+        // make sure the scheduler knows the id
1433
+        $task->setId($taskEntity->getId());
1434
+    }
1435
+
1436
+    /**
1437
+     * @param array $output
1438
+     * @param ShapeDescriptor[] ...$specs the specs that define which keys to keep
1439
+     * @return array
1440
+     * @throws NotPermittedException
1441
+     */
1442
+    private function validateOutputFileIds(array $output, ...$specs): array {
1443
+        $newOutput = [];
1444
+        $spec = array_reduce($specs, fn ($carry, $spec) => $carry + $spec, []);
1445
+        foreach ($spec as $key => $descriptor) {
1446
+            $type = $descriptor->getShapeType();
1447
+            if (!isset($output[$key])) {
1448
+                continue;
1449
+            }
1450
+            if (!in_array(EShapeType::getScalarType($type), [EShapeType::Image, EShapeType::Audio, EShapeType::Video, EShapeType::File], true)) {
1451
+                $newOutput[$key] = $output[$key];
1452
+                continue;
1453
+            }
1454
+            if (EShapeType::getScalarType($type) === $type) {
1455
+                // Is scalar file ID
1456
+                $newOutput[$key] = $this->validateFileId($output[$key]);
1457
+            } else {
1458
+                // Is list of file IDs
1459
+                $newOutput = [];
1460
+                foreach ($output[$key] as $item) {
1461
+                    $newOutput[$key][] = $this->validateFileId($item);
1462
+                }
1463
+            }
1464
+        }
1465
+        return $newOutput;
1466
+    }
1467
+
1468
+    /**
1469
+     * @param mixed $id
1470
+     * @return File
1471
+     * @throws ValidationException
1472
+     */
1473
+    private function validateFileId(mixed $id): File {
1474
+        $node = $this->rootFolder->getFirstNodeById($id);
1475
+        if ($node === null) {
1476
+            $node = $this->rootFolder->getFirstNodeByIdInPath($id, '/' . $this->rootFolder->getAppDataDirectoryName() . '/');
1477
+            if ($node === null) {
1478
+                throw new ValidationException('Could not find file ' . $id);
1479
+            } elseif (!$node instanceof File) {
1480
+                throw new ValidationException('File with id "' . $id . '" is not a file');
1481
+            }
1482
+        } elseif (!$node instanceof File) {
1483
+            throw new ValidationException('File with id "' . $id . '" is not a file');
1484
+        }
1485
+        return $node;
1486
+    }
1487
+
1488
+    /**
1489
+     * @param mixed $fileId
1490
+     * @param string|null $userId
1491
+     * @return void
1492
+     * @throws UnauthorizedException
1493
+     */
1494
+    private function validateUserAccessToFile(mixed $fileId, ?string $userId): void {
1495
+        if ($userId === null) {
1496
+            throw new UnauthorizedException('User does not have access to file ' . $fileId);
1497
+        }
1498
+        $mounts = $this->userMountCache->getMountsForFileId($fileId);
1499
+        $userIds = array_map(fn ($mount) => $mount->getUser()->getUID(), $mounts);
1500
+        if (!in_array($userId, $userIds)) {
1501
+            throw new UnauthorizedException('User ' . $userId . ' does not have access to file ' . $fileId);
1502
+        }
1503
+    }
1504
+
1505
+    /**
1506
+     * @param Task $task
1507
+     * @return list<int>
1508
+     * @throws NotFoundException
1509
+     */
1510
+    public function extractFileIdsFromTask(Task $task): array {
1511
+        $ids = [];
1512
+        $taskTypes = $this->getAvailableTaskTypes();
1513
+        if (!isset($taskTypes[$task->getTaskTypeId()])) {
1514
+            throw new NotFoundException('Could not find task type');
1515
+        }
1516
+        $taskType = $taskTypes[$task->getTaskTypeId()];
1517
+        foreach ($taskType['inputShape'] + $taskType['optionalInputShape'] as $key => $descriptor) {
1518
+            if (in_array(EShapeType::getScalarType($descriptor->getShapeType()), [EShapeType::File, EShapeType::Image, EShapeType::Audio, EShapeType::Video], true)) {
1519
+                /** @var int|list<int> $inputSlot */
1520
+                $inputSlot = $task->getInput()[$key];
1521
+                if (is_array($inputSlot)) {
1522
+                    $ids = array_merge($inputSlot, $ids);
1523
+                } else {
1524
+                    $ids[] = $inputSlot;
1525
+                }
1526
+            }
1527
+        }
1528
+        if ($task->getOutput() !== null) {
1529
+            foreach ($taskType['outputShape'] + $taskType['optionalOutputShape'] as $key => $descriptor) {
1530
+                if (in_array(EShapeType::getScalarType($descriptor->getShapeType()), [EShapeType::File, EShapeType::Image, EShapeType::Audio, EShapeType::Video], true)) {
1531
+                    /** @var int|list<int> $outputSlot */
1532
+                    $outputSlot = $task->getOutput()[$key];
1533
+                    if (is_array($outputSlot)) {
1534
+                        $ids = array_merge($outputSlot, $ids);
1535
+                    } else {
1536
+                        $ids[] = $outputSlot;
1537
+                    }
1538
+                }
1539
+            }
1540
+        }
1541
+        return $ids;
1542
+    }
1543
+
1544
+    /**
1545
+     * @param ISimpleFolder $folder
1546
+     * @param int $ageInSeconds
1547
+     * @return \Generator
1548
+     */
1549
+    public function clearFilesOlderThan(ISimpleFolder $folder, int $ageInSeconds = self::MAX_TASK_AGE_SECONDS): \Generator {
1550
+        foreach ($folder->getDirectoryListing() as $file) {
1551
+            if ($file->getMTime() < time() - $ageInSeconds) {
1552
+                try {
1553
+                    $fileName = $file->getName();
1554
+                    $file->delete();
1555
+                    yield $fileName;
1556
+                } catch (NotPermittedException $e) {
1557
+                    $this->logger->warning('Failed to delete a stale task processing file', ['exception' => $e]);
1558
+                }
1559
+            }
1560
+        }
1561
+    }
1562
+
1563
+    /**
1564
+     * @param int $ageInSeconds
1565
+     * @return \Generator
1566
+     * @throws Exception
1567
+     * @throws InvalidPathException
1568
+     * @throws NotFoundException
1569
+     * @throws \JsonException
1570
+     * @throws \OCP\Files\NotFoundException
1571
+     */
1572
+    public function cleanupTaskProcessingTaskFiles(int $ageInSeconds = self::MAX_TASK_AGE_SECONDS): \Generator {
1573
+        $taskIdsToCleanup = [];
1574
+        foreach ($this->taskMapper->getTasksToCleanup($ageInSeconds) as $task) {
1575
+            $taskIdsToCleanup[] = $task->getId();
1576
+            $ocpTask = $task->toPublicTask();
1577
+            $fileIds = $this->extractFileIdsFromTask($ocpTask);
1578
+            foreach ($fileIds as $fileId) {
1579
+                // only look for output files stored in appData/TaskProcessing/
1580
+                $file = $this->rootFolder->getFirstNodeByIdInPath($fileId, '/' . $this->rootFolder->getAppDataDirectoryName() . '/core/TaskProcessing/');
1581
+                if ($file instanceof File) {
1582
+                    try {
1583
+                        $fileId = $file->getId();
1584
+                        $fileName = $file->getName();
1585
+                        $file->delete();
1586
+                        yield ['task_id' => $task->getId(), 'file_id' => $fileId, 'file_name' => $fileName];
1587
+                    } catch (NotPermittedException $e) {
1588
+                        $this->logger->warning('Failed to delete a stale task processing file', ['exception' => $e]);
1589
+                    }
1590
+                }
1591
+            }
1592
+        }
1593
+        return $taskIdsToCleanup;
1594
+    }
1595
+
1596
+    /**
1597
+     * Make a request to the task's webhookUri if necessary
1598
+     *
1599
+     * @param Task $task
1600
+     */
1601
+    private function runWebhook(Task $task): void {
1602
+        $uri = $task->getWebhookUri();
1603
+        $method = $task->getWebhookMethod();
1604
+
1605
+        if (!$uri || !$method) {
1606
+            return;
1607
+        }
1608
+
1609
+        if (in_array($method, ['HTTP:GET', 'HTTP:POST', 'HTTP:PUT', 'HTTP:DELETE'], true)) {
1610
+            $client = $this->clientService->newClient();
1611
+            $httpMethod = preg_replace('/^HTTP:/', '', $method);
1612
+            $options = [
1613
+                'timeout' => 30,
1614
+                'body' => json_encode([
1615
+                    'task' => $task->jsonSerialize(),
1616
+                ]),
1617
+                'headers' => ['Content-Type' => 'application/json'],
1618
+            ];
1619
+            try {
1620
+                $client->request($httpMethod, $uri, $options);
1621
+            } catch (ClientException|ServerException $e) {
1622
+                $this->logger->warning('Task processing HTTP webhook failed for task ' . $task->getId() . '. Request failed', ['exception' => $e]);
1623
+            } catch (\Exception|\Throwable $e) {
1624
+                $this->logger->warning('Task processing HTTP webhook failed for task ' . $task->getId() . '. Unknown error', ['exception' => $e]);
1625
+            }
1626
+        } elseif (str_starts_with($method, 'AppAPI:') && str_starts_with($uri, '/')) {
1627
+            $parsedMethod = explode(':', $method, 4);
1628
+            if (count($parsedMethod) < 3) {
1629
+                $this->logger->warning('Task processing AppAPI webhook failed for task ' . $task->getId() . '. Invalid method: ' . $method);
1630
+            }
1631
+            [, $exAppId, $httpMethod] = $parsedMethod;
1632
+            if (!$this->appManager->isEnabledForAnyone('app_api')) {
1633
+                $this->logger->warning('Task processing AppAPI webhook failed for task ' . $task->getId() . '. AppAPI is disabled or not installed.');
1634
+                return;
1635
+            }
1636
+            try {
1637
+                $appApiFunctions = \OCP\Server::get(\OCA\AppAPI\PublicFunctions::class);
1638
+            } catch (ContainerExceptionInterface|NotFoundExceptionInterface) {
1639
+                $this->logger->warning('Task processing AppAPI webhook failed for task ' . $task->getId() . '. Could not get AppAPI public functions.');
1640
+                return;
1641
+            }
1642
+            $exApp = $appApiFunctions->getExApp($exAppId);
1643
+            if ($exApp === null) {
1644
+                $this->logger->warning('Task processing AppAPI webhook failed for task ' . $task->getId() . '. ExApp ' . $exAppId . ' is missing.');
1645
+                return;
1646
+            } elseif (!$exApp['enabled']) {
1647
+                $this->logger->warning('Task processing AppAPI webhook failed for task ' . $task->getId() . '. ExApp ' . $exAppId . ' is disabled.');
1648
+                return;
1649
+            }
1650
+            $requestParams = [
1651
+                'task' => $task->jsonSerialize(),
1652
+            ];
1653
+            $requestOptions = [
1654
+                'timeout' => 30,
1655
+            ];
1656
+            $response = $appApiFunctions->exAppRequest($exAppId, $uri, $task->getUserId(), $httpMethod, $requestParams, $requestOptions);
1657
+            if (is_array($response) && isset($response['error'])) {
1658
+                $this->logger->warning('Task processing AppAPI webhook failed for task ' . $task->getId() . '. Error during request to ExApp(' . $exAppId . '): ', $response['error']);
1659
+            }
1660
+        }
1661
+    }
1662 1662
 }
Please login to merge, or discard this patch.