Completed
Push — master ( 4ecd21...99d627 )
by Marcel
19:31
created
lib/public/TaskProcessing/Exception/UserFacingProcessingException.php 1 patch
Indentation   +30 added lines, -30 removed lines patch added patch discarded remove patch
@@ -22,34 +22,34 @@
 block discarded – undo
22 22
 #[Consumable(since: '33.0.0')]
23 23
 class UserFacingProcessingException extends ProcessingException {
24 24
 
25
-	/**
26
-	 * @param string $message
27
-	 * @param int $code
28
-	 * @param \Throwable|null $previous
29
-	 * @param string|null $userFacingMessage
30
-	 * @since 33.0.0
31
-	 */
32
-	public function __construct(
33
-		string $message = '',
34
-		int $code = 0,
35
-		?\Throwable $previous = null,
36
-		private ?string $userFacingMessage = null,
37
-	) {
38
-		parent::__construct($message, $code, $previous);
39
-	}
40
-
41
-	/**
42
-	 * @since 33.0.0
43
-	 */
44
-	public function getUserFacingMessage(): ?string {
45
-		return $this->userFacingMessage;
46
-	}
47
-
48
-	/**
49
-	 * @param null|string $userFacingMessage Must be already translated into the language of the user
50
-	 * @since 33.0.0
51
-	 */
52
-	public function setUserFacingMessage(?string $userFacingMessage): void {
53
-		$this->userFacingMessage = $userFacingMessage;
54
-	}
25
+    /**
26
+     * @param string $message
27
+     * @param int $code
28
+     * @param \Throwable|null $previous
29
+     * @param string|null $userFacingMessage
30
+     * @since 33.0.0
31
+     */
32
+    public function __construct(
33
+        string $message = '',
34
+        int $code = 0,
35
+        ?\Throwable $previous = null,
36
+        private ?string $userFacingMessage = null,
37
+    ) {
38
+        parent::__construct($message, $code, $previous);
39
+    }
40
+
41
+    /**
42
+     * @since 33.0.0
43
+     */
44
+    public function getUserFacingMessage(): ?string {
45
+        return $this->userFacingMessage;
46
+    }
47
+
48
+    /**
49
+     * @param null|string $userFacingMessage Must be already translated into the language of the user
50
+     * @since 33.0.0
51
+     */
52
+    public function setUserFacingMessage(?string $userFacingMessage): void {
53
+        $this->userFacingMessage = $userFacingMessage;
54
+    }
55 55
 }
Please login to merge, or discard this patch.
lib/public/TaskProcessing/IManager.php 1 patch
Indentation   +216 added lines, -216 removed lines patch added patch discarded remove patch
@@ -29,242 +29,242 @@
 block discarded – undo
29 29
 #[Consumable(since: '30.0.0')]
30 30
 interface IManager {
31 31
 
32
-	/**
33
-	 * @since 30.0.0
34
-	 */
35
-	public function hasProviders(): bool;
32
+    /**
33
+     * @since 30.0.0
34
+     */
35
+    public function hasProviders(): bool;
36 36
 
37
-	/**
38
-	 * @return IProvider[]
39
-	 * @since 30.0.0
40
-	 */
41
-	public function getProviders(): array;
37
+    /**
38
+     * @return IProvider[]
39
+     * @since 30.0.0
40
+     */
41
+    public function getProviders(): array;
42 42
 
43
-	/**
44
-	 * @param string $taskTypeId
45
-	 * @return IProvider
46
-	 * @throws Exception
47
-	 * @since 30.0.0
48
-	 */
49
-	public function getPreferredProvider(string $taskTypeId);
43
+    /**
44
+     * @param string $taskTypeId
45
+     * @return IProvider
46
+     * @throws Exception
47
+     * @since 30.0.0
48
+     */
49
+    public function getPreferredProvider(string $taskTypeId);
50 50
 
51
-	/**
52
-	 * @param bool $showDisabled if false, disabled task types will be filtered out
53
-	 * @param ?string $userId to check if the user is a guest. Will be obtained from session if left to default
54
-	 * @return array<string, array{name: string, description: string, inputShape: ShapeDescriptor[], inputShapeEnumValues: ShapeEnumValue[][], inputShapeDefaults: array<array-key, numeric|string>, isInternal: bool, optionalInputShape: ShapeDescriptor[], optionalInputShapeEnumValues: ShapeEnumValue[][], optionalInputShapeDefaults: array<array-key, numeric|string>, outputShape: ShapeDescriptor[], outputShapeEnumValues: ShapeEnumValue[][], optionalOutputShape: ShapeDescriptor[], optionalOutputShapeEnumValues: ShapeEnumValue[][]}>
55
-	 * @since 30.0.0
56
-	 * @since 31.0.0 Added the `showDisabled` argument.
57
-	 * @since 31.0.7 Added the `userId` argument
58
-	 * @since 33.0.0 Added `isInternal` to return value
59
-	 */
60
-	public function getAvailableTaskTypes(bool $showDisabled = false, ?string $userId = null): array;
51
+    /**
52
+     * @param bool $showDisabled if false, disabled task types will be filtered out
53
+     * @param ?string $userId to check if the user is a guest. Will be obtained from session if left to default
54
+     * @return array<string, array{name: string, description: string, inputShape: ShapeDescriptor[], inputShapeEnumValues: ShapeEnumValue[][], inputShapeDefaults: array<array-key, numeric|string>, isInternal: bool, optionalInputShape: ShapeDescriptor[], optionalInputShapeEnumValues: ShapeEnumValue[][], optionalInputShapeDefaults: array<array-key, numeric|string>, outputShape: ShapeDescriptor[], outputShapeEnumValues: ShapeEnumValue[][], optionalOutputShape: ShapeDescriptor[], optionalOutputShapeEnumValues: ShapeEnumValue[][]}>
55
+     * @since 30.0.0
56
+     * @since 31.0.0 Added the `showDisabled` argument.
57
+     * @since 31.0.7 Added the `userId` argument
58
+     * @since 33.0.0 Added `isInternal` to return value
59
+     */
60
+    public function getAvailableTaskTypes(bool $showDisabled = false, ?string $userId = null): array;
61 61
 
62
-	/**
63
-	 * @param bool $showDisabled if false, disabled task types will be filtered out
64
-	 * @param ?string $userId to check if the user is a guest. Will be obtained from session if left to default
65
-	 * @return list<string>
66
-	 * @since 32.0.0
67
-	 */
68
-	public function getAvailableTaskTypeIds(bool $showDisabled = false, ?string $userId = null): array;
62
+    /**
63
+     * @param bool $showDisabled if false, disabled task types will be filtered out
64
+     * @param ?string $userId to check if the user is a guest. Will be obtained from session if left to default
65
+     * @return list<string>
66
+     * @since 32.0.0
67
+     */
68
+    public function getAvailableTaskTypeIds(bool $showDisabled = false, ?string $userId = null): array;
69 69
 
70
-	/**
71
-	 * @param Task $task The task to run
72
-	 * @throws PreConditionNotMetException If no or not the requested provider was registered but this method was still called
73
-	 * @throws ValidationException the given task input didn't pass validation against the task type's input shape and/or the providers optional input shape specs
74
-	 * @throws Exception storing the task in the database failed
75
-	 * @throws UnauthorizedException the user scheduling the task does not have access to the files used in the input
76
-	 * @since 30.0.0
77
-	 */
78
-	public function scheduleTask(Task $task): void;
70
+    /**
71
+     * @param Task $task The task to run
72
+     * @throws PreConditionNotMetException If no or not the requested provider was registered but this method was still called
73
+     * @throws ValidationException the given task input didn't pass validation against the task type's input shape and/or the providers optional input shape specs
74
+     * @throws Exception storing the task in the database failed
75
+     * @throws UnauthorizedException the user scheduling the task does not have access to the files used in the input
76
+     * @since 30.0.0
77
+     */
78
+    public function scheduleTask(Task $task): void;
79 79
 
80
-	/**
81
-	 * Run the task and return the finished task
82
-	 *
83
-	 * @param Task $task The task to run
84
-	 * @return Task The result task
85
-	 * @throws PreConditionNotMetException If no or not the requested provider was registered but this method was still called
86
-	 * @throws ValidationException the given task input didn't pass validation against the task type's input shape and/or the providers optional input shape specs
87
-	 * @throws Exception storing the task in the database failed
88
-	 * @throws UnauthorizedException the user scheduling the task does not have access to the files used in the input
89
-	 * @since 30.0.0
90
-	 */
91
-	public function runTask(Task $task): Task;
80
+    /**
81
+     * Run the task and return the finished task
82
+     *
83
+     * @param Task $task The task to run
84
+     * @return Task The result task
85
+     * @throws PreConditionNotMetException If no or not the requested provider was registered but this method was still called
86
+     * @throws ValidationException the given task input didn't pass validation against the task type's input shape and/or the providers optional input shape specs
87
+     * @throws Exception storing the task in the database failed
88
+     * @throws UnauthorizedException the user scheduling the task does not have access to the files used in the input
89
+     * @since 30.0.0
90
+     */
91
+    public function runTask(Task $task): Task;
92 92
 
93
-	/**
94
-	 * Process task with a synchronous provider
95
-	 *
96
-	 * Prepare task input data and run the process method of the provider
97
-	 * This should only be used by OC\TaskProcessing\SynchronousBackgroundJob::run() and OCP\TaskProcessing\IManager::runTask()
98
-	 *
99
-	 * @param Task $task
100
-	 * @param ISynchronousProvider $provider
101
-	 * @return bool True if the task has run successfully
102
-	 * @throws Exception
103
-	 * @since 30.0.0
104
-	 */
105
-	public function processTask(Task $task, ISynchronousProvider $provider): bool;
93
+    /**
94
+     * Process task with a synchronous provider
95
+     *
96
+     * Prepare task input data and run the process method of the provider
97
+     * This should only be used by OC\TaskProcessing\SynchronousBackgroundJob::run() and OCP\TaskProcessing\IManager::runTask()
98
+     *
99
+     * @param Task $task
100
+     * @param ISynchronousProvider $provider
101
+     * @return bool True if the task has run successfully
102
+     * @throws Exception
103
+     * @since 30.0.0
104
+     */
105
+    public function processTask(Task $task, ISynchronousProvider $provider): bool;
106 106
 
107
-	/**
108
-	 * Delete a task that has been scheduled before
109
-	 *
110
-	 * @param Task $task The task to delete
111
-	 * @throws Exception if deleting the task in the database failed
112
-	 * @since 30.0.0
113
-	 */
114
-	public function deleteTask(Task $task): void;
107
+    /**
108
+     * Delete a task that has been scheduled before
109
+     *
110
+     * @param Task $task The task to delete
111
+     * @throws Exception if deleting the task in the database failed
112
+     * @since 30.0.0
113
+     */
114
+    public function deleteTask(Task $task): void;
115 115
 
116
-	/**
117
-	 * @param int $id The id of the task
118
-	 * @return Task
119
-	 * @throws Exception If the query failed
120
-	 * @throws NotFoundException If the task could not be found
121
-	 * @since 30.0.0
122
-	 */
123
-	public function getTask(int $id): Task;
116
+    /**
117
+     * @param int $id The id of the task
118
+     * @return Task
119
+     * @throws Exception If the query failed
120
+     * @throws NotFoundException If the task could not be found
121
+     * @since 30.0.0
122
+     */
123
+    public function getTask(int $id): Task;
124 124
 
125
-	/**
126
-	 * @param int $id The id of the task
127
-	 * @throws Exception If the query failed
128
-	 * @throws NotFoundException If the task could not be found
129
-	 * @since 30.0.0
130
-	 */
131
-	public function cancelTask(int $id): void;
125
+    /**
126
+     * @param int $id The id of the task
127
+     * @throws Exception If the query failed
128
+     * @throws NotFoundException If the task could not be found
129
+     * @since 30.0.0
130
+     */
131
+    public function cancelTask(int $id): void;
132 132
 
133
-	/**
134
-	 * @param int $id The id of the task
135
-	 * @param string|null $error
136
-	 * @param array|null $result
137
-	 * @param bool $isUsingFileIds
138
-	 * @param string|null $userFacingError
139
-	 * @throws Exception If the query failed
140
-	 * @throws NotFoundException If the task could not be found
141
-	 * @since 30.0.0
142
-	 * @since 33.0.0 Added `userFacingError` parameter
143
-	 */
144
-	public function setTaskResult(int $id, ?string $error, ?array $result, bool $isUsingFileIds = false, ?string $userFacingError = null): void;
133
+    /**
134
+     * @param int $id The id of the task
135
+     * @param string|null $error
136
+     * @param array|null $result
137
+     * @param bool $isUsingFileIds
138
+     * @param string|null $userFacingError
139
+     * @throws Exception If the query failed
140
+     * @throws NotFoundException If the task could not be found
141
+     * @since 30.0.0
142
+     * @since 33.0.0 Added `userFacingError` parameter
143
+     */
144
+    public function setTaskResult(int $id, ?string $error, ?array $result, bool $isUsingFileIds = false, ?string $userFacingError = null): void;
145 145
 
146
-	/**
147
-	 * @param int $id
148
-	 * @param float $progress
149
-	 * @return bool `true` if the task should still be running; `false` if the task has been cancelled in the meantime
150
-	 * @throws ValidationException
151
-	 * @throws Exception
152
-	 * @throws NotFoundException
153
-	 * @since 30.0.0
154
-	 */
155
-	public function setTaskProgress(int $id, float $progress): bool;
146
+    /**
147
+     * @param int $id
148
+     * @param float $progress
149
+     * @return bool `true` if the task should still be running; `false` if the task has been cancelled in the meantime
150
+     * @throws ValidationException
151
+     * @throws Exception
152
+     * @throws NotFoundException
153
+     * @since 30.0.0
154
+     */
155
+    public function setTaskProgress(int $id, float $progress): bool;
156 156
 
157
-	/**
158
-	 * @param list<string> $taskTypeIds
159
-	 * @param list<int> $taskIdsToIgnore
160
-	 * @return Task
161
-	 * @throws Exception If the query failed
162
-	 * @throws NotFoundException If no task could not be found
163
-	 * @since 30.0.0
164
-	 */
165
-	public function getNextScheduledTask(array $taskTypeIds = [], array $taskIdsToIgnore = []): Task;
157
+    /**
158
+     * @param list<string> $taskTypeIds
159
+     * @param list<int> $taskIdsToIgnore
160
+     * @return Task
161
+     * @throws Exception If the query failed
162
+     * @throws NotFoundException If no task could not be found
163
+     * @since 30.0.0
164
+     */
165
+    public function getNextScheduledTask(array $taskTypeIds = [], array $taskIdsToIgnore = []): Task;
166 166
 
167
-	/**
168
-	 * @param list<string> $taskTypeIds
169
-	 * @param list<int> $taskIdsToIgnore
170
-	 * @param int $numberOfTasks
171
-	 * @return list<Task>
172
-	 * @throws Exception If the query failed
173
-	 * @since 33.0.0
174
-	 */
175
-	public function getNextScheduledTasks(array $taskTypeIds = [], array $taskIdsToIgnore = [], int $numberOfTasks = 1): array;
167
+    /**
168
+     * @param list<string> $taskTypeIds
169
+     * @param list<int> $taskIdsToIgnore
170
+     * @param int $numberOfTasks
171
+     * @return list<Task>
172
+     * @throws Exception If the query failed
173
+     * @since 33.0.0
174
+     */
175
+    public function getNextScheduledTasks(array $taskTypeIds = [], array $taskIdsToIgnore = [], int $numberOfTasks = 1): array;
176 176
 
177
-	/**
178
-	 * @param int $id The id of the task
179
-	 * @param string|null $userId The user id that scheduled the task
180
-	 * @return Task
181
-	 * @throws Exception If the query failed
182
-	 * @throws NotFoundException If the task could not be found
183
-	 * @since 30.0.0
184
-	 */
185
-	public function getUserTask(int $id, ?string $userId): Task;
177
+    /**
178
+     * @param int $id The id of the task
179
+     * @param string|null $userId The user id that scheduled the task
180
+     * @return Task
181
+     * @throws Exception If the query failed
182
+     * @throws NotFoundException If the task could not be found
183
+     * @since 30.0.0
184
+     */
185
+    public function getUserTask(int $id, ?string $userId): Task;
186 186
 
187
-	/**
188
-	 * @param string|null $userId The user id that scheduled the task
189
-	 * @param string|null $taskTypeId The task type id to filter by
190
-	 * @param string|null $customId
191
-	 * @return list<Task>
192
-	 * @throws Exception If the query failed
193
-	 * @throws NotFoundException If the task could not be found
194
-	 * @since 30.0.0
195
-	 */
196
-	public function getUserTasks(?string $userId, ?string $taskTypeId = null, ?string $customId = null): array;
187
+    /**
188
+     * @param string|null $userId The user id that scheduled the task
189
+     * @param string|null $taskTypeId The task type id to filter by
190
+     * @param string|null $customId
191
+     * @return list<Task>
192
+     * @throws Exception If the query failed
193
+     * @throws NotFoundException If the task could not be found
194
+     * @since 30.0.0
195
+     */
196
+    public function getUserTasks(?string $userId, ?string $taskTypeId = null, ?string $customId = null): array;
197 197
 
198
-	/**
199
-	 * @param string|null $userId The user id that scheduled the task
200
-	 * @param string|null $taskTypeId The task type id to filter by
201
-	 * @param string|null $appId The app ID of the app that submitted the task
202
-	 * @param string|null $customId The custom task ID
203
-	 * @param int|null $status The task status
204
-	 * @param int|null $scheduleAfter Minimum schedule time filter
205
-	 * @param int|null $endedBefore Maximum ending time filter
206
-	 * @return list<Task>
207
-	 * @throws Exception If the query failed
208
-	 * @throws NotFoundException If the task could not be found
209
-	 * @since 30.0.0
210
-	 */
211
-	public function getTasks(
212
-		?string $userId, ?string $taskTypeId = null, ?string $appId = null, ?string $customId = null,
213
-		?int $status = null, ?int $scheduleAfter = null, ?int $endedBefore = null,
214
-	): array;
198
+    /**
199
+     * @param string|null $userId The user id that scheduled the task
200
+     * @param string|null $taskTypeId The task type id to filter by
201
+     * @param string|null $appId The app ID of the app that submitted the task
202
+     * @param string|null $customId The custom task ID
203
+     * @param int|null $status The task status
204
+     * @param int|null $scheduleAfter Minimum schedule time filter
205
+     * @param int|null $endedBefore Maximum ending time filter
206
+     * @return list<Task>
207
+     * @throws Exception If the query failed
208
+     * @throws NotFoundException If the task could not be found
209
+     * @since 30.0.0
210
+     */
211
+    public function getTasks(
212
+        ?string $userId, ?string $taskTypeId = null, ?string $appId = null, ?string $customId = null,
213
+        ?int $status = null, ?int $scheduleAfter = null, ?int $endedBefore = null,
214
+    ): array;
215 215
 
216
-	/**
217
-	 * @param string|null $userId
218
-	 * @param string $appId
219
-	 * @param string|null $customId
220
-	 * @return list<Task>
221
-	 * @throws Exception If the query failed
222
-	 * @throws \JsonException If parsing the task input and output failed
223
-	 * @since 30.0.0
224
-	 */
225
-	public function getUserTasksByApp(?string $userId, string $appId, ?string $customId = null): array;
216
+    /**
217
+     * @param string|null $userId
218
+     * @param string $appId
219
+     * @param string|null $customId
220
+     * @return list<Task>
221
+     * @throws Exception If the query failed
222
+     * @throws \JsonException If parsing the task input and output failed
223
+     * @since 30.0.0
224
+     */
225
+    public function getUserTasksByApp(?string $userId, string $appId, ?string $customId = null): array;
226 226
 
227
-	/**
228
-	 * Prepare the task's input data, so it can be processed by the provider
229
-	 * ie. this replaces file ids with File objects
230
-	 *
231
-	 * @param Task $task
232
-	 * @return array<array-key, list<numeric|string|File>|numeric|string|File>
233
-	 * @throws NotPermittedException
234
-	 * @throws GenericFileException
235
-	 * @throws LockedException
236
-	 * @throws ValidationException
237
-	 * @throws UnauthorizedException
238
-	 * @since 30.0.0
239
-	 */
240
-	public function prepareInputData(Task $task): array;
227
+    /**
228
+     * Prepare the task's input data, so it can be processed by the provider
229
+     * ie. this replaces file ids with File objects
230
+     *
231
+     * @param Task $task
232
+     * @return array<array-key, list<numeric|string|File>|numeric|string|File>
233
+     * @throws NotPermittedException
234
+     * @throws GenericFileException
235
+     * @throws LockedException
236
+     * @throws ValidationException
237
+     * @throws UnauthorizedException
238
+     * @since 30.0.0
239
+     */
240
+    public function prepareInputData(Task $task): array;
241 241
 
242
-	/**
243
-	 * Changes the task status to STATUS_RUNNING and, if successful, returns True.
244
-	 *
245
-	 * @param Task $task
246
-	 * @return bool
247
-	 * @since 30.0.0
248
-	 */
249
-	public function lockTask(Task $task): bool;
242
+    /**
243
+     * Changes the task status to STATUS_RUNNING and, if successful, returns True.
244
+     *
245
+     * @param Task $task
246
+     * @return bool
247
+     * @since 30.0.0
248
+     */
249
+    public function lockTask(Task $task): bool;
250 250
 
251
-	/**
252
-	 * @param Task $task
253
-	 * @psalm-param Task::STATUS_* $status
254
-	 * @param int $status
255
-	 * @throws \JsonException
256
-	 * @throws Exception
257
-	 * @since 30.0.0
258
-	 */
259
-	public function setTaskStatus(Task $task, int $status): void;
251
+    /**
252
+     * @param Task $task
253
+     * @psalm-param Task::STATUS_* $status
254
+     * @param int $status
255
+     * @throws \JsonException
256
+     * @throws Exception
257
+     * @since 30.0.0
258
+     */
259
+    public function setTaskStatus(Task $task, int $status): void;
260 260
 
261
-	/**
262
-	 * Extract all input and output file IDs from a task
263
-	 *
264
-	 * @param Task $task
265
-	 * @return list<int>
266
-	 * @throws NotFoundException
267
-	 * @since 32.0.0
268
-	 */
269
-	public function extractFileIdsFromTask(Task $task): array;
261
+    /**
262
+     * Extract all input and output file IDs from a task
263
+     *
264
+     * @param Task $task
265
+     * @return list<int>
266
+     * @throws NotFoundException
267
+     * @since 32.0.0
268
+     */
269
+    public function extractFileIdsFromTask(Task $task): array;
270 270
 }
Please login to merge, or discard this patch.
lib/public/TaskProcessing/Task.php 1 patch
Indentation   +392 added lines, -392 removed lines patch added patch discarded remove patch
@@ -18,396 +18,396 @@
 block discarded – undo
18 18
  * @since 30.0.0
19 19
  */
20 20
 final class Task implements \JsonSerializable {
21
-	protected ?int $id = null;
22
-
23
-	protected ?DateTime $completionExpectedAt = null;
24
-
25
-	protected ?array $output = null;
26
-
27
-	protected ?string $errorMessage = null;
28
-
29
-	protected ?float $progress = null;
30
-
31
-	protected int $lastUpdated;
32
-
33
-	protected ?string $webhookUri = null;
34
-
35
-	protected ?string $webhookMethod = null;
36
-
37
-	/**
38
-	 * @psalm-var self::STATUS_*
39
-	 */
40
-	protected int $status = self::STATUS_UNKNOWN;
41
-
42
-	protected ?int $scheduledAt = null;
43
-
44
-	protected ?int $startedAt = null;
45
-
46
-	protected ?int $endedAt = null;
47
-
48
-	protected bool $allowCleanup = true;
49
-
50
-	protected ?string $userFacingErrorMessage = null;
51
-
52
-	/**
53
-	 * @since 30.0.0
54
-	 */
55
-	public const STATUS_CANCELLED = 5;
56
-	/**
57
-	 * @since 30.0.0
58
-	 */
59
-	public const STATUS_FAILED = 4;
60
-	/**
61
-	 * @since 30.0.0
62
-	 */
63
-	public const STATUS_SUCCESSFUL = 3;
64
-	/**
65
-	 * @since 30.0.0
66
-	 */
67
-	public const STATUS_RUNNING = 2;
68
-	/**
69
-	 * @since 30.0.0
70
-	 */
71
-	public const STATUS_SCHEDULED = 1;
72
-	/**
73
-	 * @since 30.0.0
74
-	 */
75
-	public const STATUS_UNKNOWN = 0;
76
-
77
-
78
-	/**
79
-	 * @param string $taskTypeId
80
-	 * @param array<string,list<numeric|string>|numeric|string> $input
81
-	 * @param string $appId
82
-	 * @param string|null $userId
83
-	 * @param null|string $customId An arbitrary customId for this task. max length: 255 chars
84
-	 * @since 30.0.0
85
-	 */
86
-	final public function __construct(
87
-		protected readonly string $taskTypeId,
88
-		protected array $input,
89
-		protected readonly string $appId,
90
-		protected readonly ?string $userId,
91
-		protected readonly ?string $customId = '',
92
-	) {
93
-		$this->lastUpdated = time();
94
-	}
95
-
96
-	/**
97
-	 * @since 30.0.0
98
-	 */
99
-	final public function getTaskTypeId(): string {
100
-		return $this->taskTypeId;
101
-	}
102
-
103
-	/**
104
-	 * @psalm-return self::STATUS_*
105
-	 * @since 30.0.0
106
-	 */
107
-	final public function getStatus(): int {
108
-		return $this->status;
109
-	}
110
-
111
-	/**
112
-	 * @psalm-param self::STATUS_* $status
113
-	 * @since 30.0.0
114
-	 */
115
-	final public function setStatus(int $status): void {
116
-		$this->status = $status;
117
-	}
118
-
119
-	/**
120
-	 * @param ?DateTime $at
121
-	 * @since 30.0.0
122
-	 */
123
-	final public function setCompletionExpectedAt(?DateTime $at): void {
124
-		$this->completionExpectedAt = $at;
125
-	}
126
-
127
-	/**
128
-	 * @return ?DateTime
129
-	 * @since 30.0.0
130
-	 */
131
-	final public function getCompletionExpectedAt(): ?DateTime {
132
-		return $this->completionExpectedAt;
133
-	}
134
-
135
-	/**
136
-	 * @return int|null
137
-	 * @since 30.0.0
138
-	 */
139
-	final public function getId(): ?int {
140
-		return $this->id;
141
-	}
142
-
143
-	/**
144
-	 * @param int|null $id
145
-	 * @since 30.0.0
146
-	 */
147
-	final public function setId(?int $id): void {
148
-		$this->id = $id;
149
-	}
150
-
151
-	/**
152
-	 * @param null|array<array-key, list<numeric|string>|numeric|string> $output
153
-	 * @since 30.0.0
154
-	 */
155
-	final public function setOutput(?array $output): void {
156
-		$this->output = $output;
157
-	}
158
-
159
-	/**
160
-	 * @return array<array-key, list<numeric|string>|numeric|string>|null
161
-	 * @since 30.0.0
162
-	 */
163
-	final public function getOutput(): ?array {
164
-		return $this->output;
165
-	}
166
-
167
-	/**
168
-	 * @return array<array-key, list<numeric|string>|numeric|string>
169
-	 * @since 30.0.0
170
-	 */
171
-	final public function getInput(): array {
172
-		return $this->input;
173
-	}
174
-
175
-	/**
176
-	 * @return string
177
-	 * @since 30.0.0
178
-	 */
179
-	final public function getAppId(): string {
180
-		return $this->appId;
181
-	}
182
-
183
-	/**
184
-	 * @return null|string
185
-	 * @since 30.0.0
186
-	 */
187
-	final public function getCustomId(): ?string {
188
-		return $this->customId;
189
-	}
190
-
191
-	/**
192
-	 * @return string|null
193
-	 * @since 30.0.0
194
-	 */
195
-	final public function getUserId(): ?string {
196
-		return $this->userId;
197
-	}
198
-
199
-	/**
200
-	 * @return int
201
-	 * @since 30.0.0
202
-	 */
203
-	final public function getLastUpdated(): int {
204
-		return $this->lastUpdated;
205
-	}
206
-
207
-	/**
208
-	 * @param int $lastUpdated
209
-	 * @since 30.0.0
210
-	 */
211
-	final public function setLastUpdated(int $lastUpdated): void {
212
-		$this->lastUpdated = $lastUpdated;
213
-	}
214
-
215
-	/**
216
-	 * @return int|null
217
-	 * @since 30.0.0
218
-	 */
219
-	final public function getScheduledAt(): ?int {
220
-		return $this->scheduledAt;
221
-	}
222
-
223
-	/**
224
-	 * @param int|null $scheduledAt
225
-	 * @since 30.0.0
226
-	 */
227
-	final public function setScheduledAt(?int $scheduledAt): void {
228
-		$this->scheduledAt = $scheduledAt;
229
-	}
230
-
231
-	/**
232
-	 * @return int|null
233
-	 * @since 30.0.0
234
-	 */
235
-	final public function getStartedAt(): ?int {
236
-		return $this->startedAt;
237
-	}
238
-
239
-	/**
240
-	 * @param int|null $startedAt
241
-	 * @since 30.0.0
242
-	 */
243
-	final public function setStartedAt(?int $startedAt): void {
244
-		$this->startedAt = $startedAt;
245
-	}
246
-
247
-	/**
248
-	 * @return int|null
249
-	 * @since 30.0.0
250
-	 */
251
-	final public function getEndedAt(): ?int {
252
-		return $this->endedAt;
253
-	}
254
-
255
-	/**
256
-	 * @param int|null $endedAt
257
-	 * @since 30.0.0
258
-	 */
259
-	final public function setEndedAt(?int $endedAt): void {
260
-		$this->endedAt = $endedAt;
261
-	}
262
-
263
-	/**
264
-	 * @return bool
265
-	 * @since 32.0.0
266
-	 */
267
-	final public function getAllowCleanup(): bool {
268
-		return $this->allowCleanup;
269
-	}
270
-
271
-	/**
272
-	 * @param bool $allowCleanup
273
-	 * @since 32.0.0
274
-	 */
275
-	final public function setAllowCleanup(bool $allowCleanup): void {
276
-		$this->allowCleanup = $allowCleanup;
277
-	}
278
-
279
-	/**
280
-	 * @psalm-return array{id: int, lastUpdated: int, type: string, status: 'STATUS_CANCELLED'|'STATUS_FAILED'|'STATUS_SUCCESSFUL'|'STATUS_RUNNING'|'STATUS_SCHEDULED'|'STATUS_UNKNOWN', userId: ?string, appId: string, input: array<string, list<numeric|string>|numeric|string>, output: ?array<string, list<numeric|string>|numeric|string>, customId: ?string, completionExpectedAt: ?int, progress: ?float, scheduledAt: ?int, startedAt: ?int, endedAt: ?int, allowCleanup: bool}
281
-	 * @since 30.0.0
282
-	 */
283
-	final public function jsonSerialize(): array {
284
-		return [
285
-			'id' => (int)$this->getId(),
286
-			'type' => $this->getTaskTypeId(),
287
-			'lastUpdated' => $this->getLastUpdated(),
288
-			'status' => self::statusToString($this->getStatus()),
289
-			'userId' => $this->getUserId(),
290
-			'appId' => $this->getAppId(),
291
-			'input' => $this->getInput(),
292
-			'output' => $this->getOutput(),
293
-			'customId' => $this->getCustomId(),
294
-			'completionExpectedAt' => $this->getCompletionExpectedAt()?->getTimestamp(),
295
-			'progress' => $this->getProgress(),
296
-			'scheduledAt' => $this->getScheduledAt(),
297
-			'startedAt' => $this->getStartedAt(),
298
-			'endedAt' => $this->getEndedAt(),
299
-			'allowCleanup' => $this->getAllowCleanup(),
300
-		];
301
-	}
302
-
303
-	/**
304
-	 * @param string|null $error
305
-	 * @return void
306
-	 * @since 30.0.0
307
-	 */
308
-	final public function setErrorMessage(?string $error) {
309
-		$this->errorMessage = $error;
310
-	}
311
-
312
-	/**
313
-	 * @return string|null
314
-	 * @since 30.0.0
315
-	 */
316
-	final public function getErrorMessage(): ?string {
317
-		return $this->errorMessage;
318
-	}
319
-
320
-	/**
321
-	 * @param array $input
322
-	 * @return void
323
-	 * @since 30.0.0
324
-	 */
325
-	final public function setInput(array $input): void {
326
-		$this->input = $input;
327
-	}
328
-
329
-	/**
330
-	 * @param float|null $progress
331
-	 * @return void
332
-	 * @throws ValidationException
333
-	 * @since 30.0.0
334
-	 */
335
-	final public function setProgress(?float $progress): void {
336
-		if ($progress < 0 || $progress > 1.0) {
337
-			throw new ValidationException('Progress must be between 0.0 and 1.0 inclusively; ' . $progress . ' given');
338
-		}
339
-		$this->progress = $progress;
340
-	}
341
-
342
-	/**
343
-	 * @return float|null
344
-	 * @since 30.0.0
345
-	 */
346
-	final public function getProgress(): ?float {
347
-		return $this->progress;
348
-	}
349
-
350
-	/**
351
-	 * @return null|string
352
-	 * @since 30.0.0
353
-	 */
354
-	final public function getWebhookUri(): ?string {
355
-		return $this->webhookUri;
356
-	}
357
-
358
-	/**
359
-	 * @param string|null $webhookUri
360
-	 * @return void
361
-	 * @since 30.0.0
362
-	 */
363
-	final public function setWebhookUri(?string $webhookUri): void {
364
-		$this->webhookUri = $webhookUri;
365
-	}
366
-
367
-	/**
368
-	 * @return null|string
369
-	 * @since 30.0.0
370
-	 */
371
-	final public function getWebhookMethod(): ?string {
372
-		return $this->webhookMethod;
373
-	}
374
-
375
-	/**
376
-	 * @param string|null $webhookMethod
377
-	 * @return void
378
-	 * @since 30.0.0
379
-	 */
380
-	final public function setWebhookMethod(?string $webhookMethod): void {
381
-		$this->webhookMethod = $webhookMethod;
382
-	}
383
-
384
-	/**
385
-	 * @param int $status
386
-	 * @return 'STATUS_CANCELLED'|'STATUS_FAILED'|'STATUS_SUCCESSFUL'|'STATUS_RUNNING'|'STATUS_SCHEDULED'|'STATUS_UNKNOWN'
387
-	 * @since 30.0.0
388
-	 */
389
-	final public static function statusToString(int $status): string {
390
-		return match ($status) {
391
-			self::STATUS_CANCELLED => 'STATUS_CANCELLED',
392
-			self::STATUS_FAILED => 'STATUS_FAILED',
393
-			self::STATUS_SUCCESSFUL => 'STATUS_SUCCESSFUL',
394
-			self::STATUS_RUNNING => 'STATUS_RUNNING',
395
-			self::STATUS_SCHEDULED => 'STATUS_SCHEDULED',
396
-			default => 'STATUS_UNKNOWN',
397
-		};
398
-	}
399
-
400
-	/**
401
-	 * @since 33.0.0
402
-	 */
403
-	public function setUserFacingErrorMessage(?string $userFacingErrorMessage): void {
404
-		$this->userFacingErrorMessage = $userFacingErrorMessage;
405
-	}
406
-
407
-	/**
408
-	 * @since 33.0.0
409
-	 */
410
-	public function getUserFacingErrorMessage(): ?string {
411
-		return $this->userFacingErrorMessage;
412
-	}
21
+    protected ?int $id = null;
22
+
23
+    protected ?DateTime $completionExpectedAt = null;
24
+
25
+    protected ?array $output = null;
26
+
27
+    protected ?string $errorMessage = null;
28
+
29
+    protected ?float $progress = null;
30
+
31
+    protected int $lastUpdated;
32
+
33
+    protected ?string $webhookUri = null;
34
+
35
+    protected ?string $webhookMethod = null;
36
+
37
+    /**
38
+     * @psalm-var self::STATUS_*
39
+     */
40
+    protected int $status = self::STATUS_UNKNOWN;
41
+
42
+    protected ?int $scheduledAt = null;
43
+
44
+    protected ?int $startedAt = null;
45
+
46
+    protected ?int $endedAt = null;
47
+
48
+    protected bool $allowCleanup = true;
49
+
50
+    protected ?string $userFacingErrorMessage = null;
51
+
52
+    /**
53
+     * @since 30.0.0
54
+     */
55
+    public const STATUS_CANCELLED = 5;
56
+    /**
57
+     * @since 30.0.0
58
+     */
59
+    public const STATUS_FAILED = 4;
60
+    /**
61
+     * @since 30.0.0
62
+     */
63
+    public const STATUS_SUCCESSFUL = 3;
64
+    /**
65
+     * @since 30.0.0
66
+     */
67
+    public const STATUS_RUNNING = 2;
68
+    /**
69
+     * @since 30.0.0
70
+     */
71
+    public const STATUS_SCHEDULED = 1;
72
+    /**
73
+     * @since 30.0.0
74
+     */
75
+    public const STATUS_UNKNOWN = 0;
76
+
77
+
78
+    /**
79
+     * @param string $taskTypeId
80
+     * @param array<string,list<numeric|string>|numeric|string> $input
81
+     * @param string $appId
82
+     * @param string|null $userId
83
+     * @param null|string $customId An arbitrary customId for this task. max length: 255 chars
84
+     * @since 30.0.0
85
+     */
86
+    final public function __construct(
87
+        protected readonly string $taskTypeId,
88
+        protected array $input,
89
+        protected readonly string $appId,
90
+        protected readonly ?string $userId,
91
+        protected readonly ?string $customId = '',
92
+    ) {
93
+        $this->lastUpdated = time();
94
+    }
95
+
96
+    /**
97
+     * @since 30.0.0
98
+     */
99
+    final public function getTaskTypeId(): string {
100
+        return $this->taskTypeId;
101
+    }
102
+
103
+    /**
104
+     * @psalm-return self::STATUS_*
105
+     * @since 30.0.0
106
+     */
107
+    final public function getStatus(): int {
108
+        return $this->status;
109
+    }
110
+
111
+    /**
112
+     * @psalm-param self::STATUS_* $status
113
+     * @since 30.0.0
114
+     */
115
+    final public function setStatus(int $status): void {
116
+        $this->status = $status;
117
+    }
118
+
119
+    /**
120
+     * @param ?DateTime $at
121
+     * @since 30.0.0
122
+     */
123
+    final public function setCompletionExpectedAt(?DateTime $at): void {
124
+        $this->completionExpectedAt = $at;
125
+    }
126
+
127
+    /**
128
+     * @return ?DateTime
129
+     * @since 30.0.0
130
+     */
131
+    final public function getCompletionExpectedAt(): ?DateTime {
132
+        return $this->completionExpectedAt;
133
+    }
134
+
135
+    /**
136
+     * @return int|null
137
+     * @since 30.0.0
138
+     */
139
+    final public function getId(): ?int {
140
+        return $this->id;
141
+    }
142
+
143
+    /**
144
+     * @param int|null $id
145
+     * @since 30.0.0
146
+     */
147
+    final public function setId(?int $id): void {
148
+        $this->id = $id;
149
+    }
150
+
151
+    /**
152
+     * @param null|array<array-key, list<numeric|string>|numeric|string> $output
153
+     * @since 30.0.0
154
+     */
155
+    final public function setOutput(?array $output): void {
156
+        $this->output = $output;
157
+    }
158
+
159
+    /**
160
+     * @return array<array-key, list<numeric|string>|numeric|string>|null
161
+     * @since 30.0.0
162
+     */
163
+    final public function getOutput(): ?array {
164
+        return $this->output;
165
+    }
166
+
167
+    /**
168
+     * @return array<array-key, list<numeric|string>|numeric|string>
169
+     * @since 30.0.0
170
+     */
171
+    final public function getInput(): array {
172
+        return $this->input;
173
+    }
174
+
175
+    /**
176
+     * @return string
177
+     * @since 30.0.0
178
+     */
179
+    final public function getAppId(): string {
180
+        return $this->appId;
181
+    }
182
+
183
+    /**
184
+     * @return null|string
185
+     * @since 30.0.0
186
+     */
187
+    final public function getCustomId(): ?string {
188
+        return $this->customId;
189
+    }
190
+
191
+    /**
192
+     * @return string|null
193
+     * @since 30.0.0
194
+     */
195
+    final public function getUserId(): ?string {
196
+        return $this->userId;
197
+    }
198
+
199
+    /**
200
+     * @return int
201
+     * @since 30.0.0
202
+     */
203
+    final public function getLastUpdated(): int {
204
+        return $this->lastUpdated;
205
+    }
206
+
207
+    /**
208
+     * @param int $lastUpdated
209
+     * @since 30.0.0
210
+     */
211
+    final public function setLastUpdated(int $lastUpdated): void {
212
+        $this->lastUpdated = $lastUpdated;
213
+    }
214
+
215
+    /**
216
+     * @return int|null
217
+     * @since 30.0.0
218
+     */
219
+    final public function getScheduledAt(): ?int {
220
+        return $this->scheduledAt;
221
+    }
222
+
223
+    /**
224
+     * @param int|null $scheduledAt
225
+     * @since 30.0.0
226
+     */
227
+    final public function setScheduledAt(?int $scheduledAt): void {
228
+        $this->scheduledAt = $scheduledAt;
229
+    }
230
+
231
+    /**
232
+     * @return int|null
233
+     * @since 30.0.0
234
+     */
235
+    final public function getStartedAt(): ?int {
236
+        return $this->startedAt;
237
+    }
238
+
239
+    /**
240
+     * @param int|null $startedAt
241
+     * @since 30.0.0
242
+     */
243
+    final public function setStartedAt(?int $startedAt): void {
244
+        $this->startedAt = $startedAt;
245
+    }
246
+
247
+    /**
248
+     * @return int|null
249
+     * @since 30.0.0
250
+     */
251
+    final public function getEndedAt(): ?int {
252
+        return $this->endedAt;
253
+    }
254
+
255
+    /**
256
+     * @param int|null $endedAt
257
+     * @since 30.0.0
258
+     */
259
+    final public function setEndedAt(?int $endedAt): void {
260
+        $this->endedAt = $endedAt;
261
+    }
262
+
263
+    /**
264
+     * @return bool
265
+     * @since 32.0.0
266
+     */
267
+    final public function getAllowCleanup(): bool {
268
+        return $this->allowCleanup;
269
+    }
270
+
271
+    /**
272
+     * @param bool $allowCleanup
273
+     * @since 32.0.0
274
+     */
275
+    final public function setAllowCleanup(bool $allowCleanup): void {
276
+        $this->allowCleanup = $allowCleanup;
277
+    }
278
+
279
+    /**
280
+     * @psalm-return array{id: int, lastUpdated: int, type: string, status: 'STATUS_CANCELLED'|'STATUS_FAILED'|'STATUS_SUCCESSFUL'|'STATUS_RUNNING'|'STATUS_SCHEDULED'|'STATUS_UNKNOWN', userId: ?string, appId: string, input: array<string, list<numeric|string>|numeric|string>, output: ?array<string, list<numeric|string>|numeric|string>, customId: ?string, completionExpectedAt: ?int, progress: ?float, scheduledAt: ?int, startedAt: ?int, endedAt: ?int, allowCleanup: bool}
281
+     * @since 30.0.0
282
+     */
283
+    final public function jsonSerialize(): array {
284
+        return [
285
+            'id' => (int)$this->getId(),
286
+            'type' => $this->getTaskTypeId(),
287
+            'lastUpdated' => $this->getLastUpdated(),
288
+            'status' => self::statusToString($this->getStatus()),
289
+            'userId' => $this->getUserId(),
290
+            'appId' => $this->getAppId(),
291
+            'input' => $this->getInput(),
292
+            'output' => $this->getOutput(),
293
+            'customId' => $this->getCustomId(),
294
+            'completionExpectedAt' => $this->getCompletionExpectedAt()?->getTimestamp(),
295
+            'progress' => $this->getProgress(),
296
+            'scheduledAt' => $this->getScheduledAt(),
297
+            'startedAt' => $this->getStartedAt(),
298
+            'endedAt' => $this->getEndedAt(),
299
+            'allowCleanup' => $this->getAllowCleanup(),
300
+        ];
301
+    }
302
+
303
+    /**
304
+     * @param string|null $error
305
+     * @return void
306
+     * @since 30.0.0
307
+     */
308
+    final public function setErrorMessage(?string $error) {
309
+        $this->errorMessage = $error;
310
+    }
311
+
312
+    /**
313
+     * @return string|null
314
+     * @since 30.0.0
315
+     */
316
+    final public function getErrorMessage(): ?string {
317
+        return $this->errorMessage;
318
+    }
319
+
320
+    /**
321
+     * @param array $input
322
+     * @return void
323
+     * @since 30.0.0
324
+     */
325
+    final public function setInput(array $input): void {
326
+        $this->input = $input;
327
+    }
328
+
329
+    /**
330
+     * @param float|null $progress
331
+     * @return void
332
+     * @throws ValidationException
333
+     * @since 30.0.0
334
+     */
335
+    final public function setProgress(?float $progress): void {
336
+        if ($progress < 0 || $progress > 1.0) {
337
+            throw new ValidationException('Progress must be between 0.0 and 1.0 inclusively; ' . $progress . ' given');
338
+        }
339
+        $this->progress = $progress;
340
+    }
341
+
342
+    /**
343
+     * @return float|null
344
+     * @since 30.0.0
345
+     */
346
+    final public function getProgress(): ?float {
347
+        return $this->progress;
348
+    }
349
+
350
+    /**
351
+     * @return null|string
352
+     * @since 30.0.0
353
+     */
354
+    final public function getWebhookUri(): ?string {
355
+        return $this->webhookUri;
356
+    }
357
+
358
+    /**
359
+     * @param string|null $webhookUri
360
+     * @return void
361
+     * @since 30.0.0
362
+     */
363
+    final public function setWebhookUri(?string $webhookUri): void {
364
+        $this->webhookUri = $webhookUri;
365
+    }
366
+
367
+    /**
368
+     * @return null|string
369
+     * @since 30.0.0
370
+     */
371
+    final public function getWebhookMethod(): ?string {
372
+        return $this->webhookMethod;
373
+    }
374
+
375
+    /**
376
+     * @param string|null $webhookMethod
377
+     * @return void
378
+     * @since 30.0.0
379
+     */
380
+    final public function setWebhookMethod(?string $webhookMethod): void {
381
+        $this->webhookMethod = $webhookMethod;
382
+    }
383
+
384
+    /**
385
+     * @param int $status
386
+     * @return 'STATUS_CANCELLED'|'STATUS_FAILED'|'STATUS_SUCCESSFUL'|'STATUS_RUNNING'|'STATUS_SCHEDULED'|'STATUS_UNKNOWN'
387
+     * @since 30.0.0
388
+     */
389
+    final public static function statusToString(int $status): string {
390
+        return match ($status) {
391
+            self::STATUS_CANCELLED => 'STATUS_CANCELLED',
392
+            self::STATUS_FAILED => 'STATUS_FAILED',
393
+            self::STATUS_SUCCESSFUL => 'STATUS_SUCCESSFUL',
394
+            self::STATUS_RUNNING => 'STATUS_RUNNING',
395
+            self::STATUS_SCHEDULED => 'STATUS_SCHEDULED',
396
+            default => 'STATUS_UNKNOWN',
397
+        };
398
+    }
399
+
400
+    /**
401
+     * @since 33.0.0
402
+     */
403
+    public function setUserFacingErrorMessage(?string $userFacingErrorMessage): void {
404
+        $this->userFacingErrorMessage = $userFacingErrorMessage;
405
+    }
406
+
407
+    /**
408
+     * @since 33.0.0
409
+     */
410
+    public function getUserFacingErrorMessage(): ?string {
411
+        return $this->userFacingErrorMessage;
412
+    }
413 413
 }
Please login to merge, or discard this patch.
lib/private/TaskProcessing/Db/Task.php 1 patch
Indentation   +100 added lines, -100 removed lines patch added patch discarded remove patch
@@ -51,111 +51,111 @@
 block discarded – undo
51 51
  * @method null|string getUserFacingErrorMessage()
52 52
  */
53 53
 class Task extends Entity {
54
-	protected $lastUpdated;
55
-	protected $type;
56
-	protected $input;
57
-	protected $output;
58
-	protected $status;
59
-	protected $userId;
60
-	protected $appId;
61
-	protected $customId;
62
-	protected $completionExpectedAt;
63
-	protected $errorMessage;
64
-	protected $progress;
65
-	protected $webhookUri;
66
-	protected $webhookMethod;
67
-	protected $scheduledAt;
68
-	protected $startedAt;
69
-	protected $endedAt;
70
-	protected $allowCleanup;
71
-	protected $userFacingErrorMessage;
54
+    protected $lastUpdated;
55
+    protected $type;
56
+    protected $input;
57
+    protected $output;
58
+    protected $status;
59
+    protected $userId;
60
+    protected $appId;
61
+    protected $customId;
62
+    protected $completionExpectedAt;
63
+    protected $errorMessage;
64
+    protected $progress;
65
+    protected $webhookUri;
66
+    protected $webhookMethod;
67
+    protected $scheduledAt;
68
+    protected $startedAt;
69
+    protected $endedAt;
70
+    protected $allowCleanup;
71
+    protected $userFacingErrorMessage;
72 72
 
73
-	/**
74
-	 * @var string[]
75
-	 */
76
-	public static array $columns = ['id', 'last_updated', 'type', 'input', 'output', 'status', 'user_id', 'app_id', 'custom_id', 'completion_expected_at', 'error_message', 'progress', 'webhook_uri', 'webhook_method', 'scheduled_at', 'started_at', 'ended_at', 'allow_cleanup', 'user_facing_error_message'];
73
+    /**
74
+     * @var string[]
75
+     */
76
+    public static array $columns = ['id', 'last_updated', 'type', 'input', 'output', 'status', 'user_id', 'app_id', 'custom_id', 'completion_expected_at', 'error_message', 'progress', 'webhook_uri', 'webhook_method', 'scheduled_at', 'started_at', 'ended_at', 'allow_cleanup', 'user_facing_error_message'];
77 77
 
78
-	/**
79
-	 * @var string[]
80
-	 */
81
-	public static array $fields = ['id', 'lastUpdated', 'type', 'input', 'output', 'status', 'userId', 'appId', 'customId', 'completionExpectedAt', 'errorMessage', 'progress', 'webhookUri', 'webhookMethod', 'scheduledAt', 'startedAt', 'endedAt', 'allowCleanup', 'userFacingErrorMessage'];
78
+    /**
79
+     * @var string[]
80
+     */
81
+    public static array $fields = ['id', 'lastUpdated', 'type', 'input', 'output', 'status', 'userId', 'appId', 'customId', 'completionExpectedAt', 'errorMessage', 'progress', 'webhookUri', 'webhookMethod', 'scheduledAt', 'startedAt', 'endedAt', 'allowCleanup', 'userFacingErrorMessage'];
82 82
 
83 83
 
84
-	public function __construct() {
85
-		// add types in constructor
86
-		$this->addType('id', 'integer');
87
-		$this->addType('lastUpdated', 'integer');
88
-		$this->addType('type', 'string');
89
-		$this->addType('input', 'string');
90
-		$this->addType('output', 'string');
91
-		$this->addType('status', 'integer');
92
-		$this->addType('userId', 'string');
93
-		$this->addType('appId', 'string');
94
-		$this->addType('customId', 'string');
95
-		$this->addType('completionExpectedAt', 'datetime');
96
-		$this->addType('errorMessage', 'string');
97
-		$this->addType('progress', 'float');
98
-		$this->addType('webhookUri', 'string');
99
-		$this->addType('webhookMethod', 'string');
100
-		$this->addType('scheduledAt', 'integer');
101
-		$this->addType('startedAt', 'integer');
102
-		$this->addType('endedAt', 'integer');
103
-		$this->addType('allowCleanup', 'integer');
104
-		$this->addType('userFacingErrorMessage', 'string');
105
-	}
84
+    public function __construct() {
85
+        // add types in constructor
86
+        $this->addType('id', 'integer');
87
+        $this->addType('lastUpdated', 'integer');
88
+        $this->addType('type', 'string');
89
+        $this->addType('input', 'string');
90
+        $this->addType('output', 'string');
91
+        $this->addType('status', 'integer');
92
+        $this->addType('userId', 'string');
93
+        $this->addType('appId', 'string');
94
+        $this->addType('customId', 'string');
95
+        $this->addType('completionExpectedAt', 'datetime');
96
+        $this->addType('errorMessage', 'string');
97
+        $this->addType('progress', 'float');
98
+        $this->addType('webhookUri', 'string');
99
+        $this->addType('webhookMethod', 'string');
100
+        $this->addType('scheduledAt', 'integer');
101
+        $this->addType('startedAt', 'integer');
102
+        $this->addType('endedAt', 'integer');
103
+        $this->addType('allowCleanup', 'integer');
104
+        $this->addType('userFacingErrorMessage', 'string');
105
+    }
106 106
 
107
-	public function toRow(): array {
108
-		return array_combine(self::$columns, array_map(function ($field) {
109
-			return $this->{'get' . ucfirst($field)}();
110
-		}, self::$fields));
111
-	}
107
+    public function toRow(): array {
108
+        return array_combine(self::$columns, array_map(function ($field) {
109
+            return $this->{'get' . ucfirst($field)}();
110
+        }, self::$fields));
111
+    }
112 112
 
113
-	public static function fromPublicTask(OCPTask $task): self {
114
-		/** @var Task $taskEntity */
115
-		$taskEntity = self::fromParams([
116
-			'id' => $task->getId(),
117
-			'type' => $task->getTaskTypeId(),
118
-			'lastUpdated' => time(),
119
-			'status' => $task->getStatus(),
120
-			'input' => json_encode($task->getInput(), JSON_THROW_ON_ERROR),
121
-			'output' => json_encode($task->getOutput(), JSON_THROW_ON_ERROR),
122
-			'errorMessage' => $task->getErrorMessage(),
123
-			'userId' => $task->getUserId(),
124
-			'appId' => $task->getAppId(),
125
-			'customId' => $task->getCustomId(),
126
-			'completionExpectedAt' => $task->getCompletionExpectedAt(),
127
-			'progress' => $task->getProgress(),
128
-			'webhookUri' => $task->getWebhookUri(),
129
-			'webhookMethod' => $task->getWebhookMethod(),
130
-			'scheduledAt' => $task->getScheduledAt(),
131
-			'startedAt' => $task->getStartedAt(),
132
-			'endedAt' => $task->getEndedAt(),
133
-			'allowCleanup' => $task->getAllowCleanup() ? 1 : 0,
134
-			'userFacingErrorMessage' => $task->getUserFacingErrorMessage(),
135
-		]);
136
-		return $taskEntity;
137
-	}
113
+    public static function fromPublicTask(OCPTask $task): self {
114
+        /** @var Task $taskEntity */
115
+        $taskEntity = self::fromParams([
116
+            'id' => $task->getId(),
117
+            'type' => $task->getTaskTypeId(),
118
+            'lastUpdated' => time(),
119
+            'status' => $task->getStatus(),
120
+            'input' => json_encode($task->getInput(), JSON_THROW_ON_ERROR),
121
+            'output' => json_encode($task->getOutput(), JSON_THROW_ON_ERROR),
122
+            'errorMessage' => $task->getErrorMessage(),
123
+            'userId' => $task->getUserId(),
124
+            'appId' => $task->getAppId(),
125
+            'customId' => $task->getCustomId(),
126
+            'completionExpectedAt' => $task->getCompletionExpectedAt(),
127
+            'progress' => $task->getProgress(),
128
+            'webhookUri' => $task->getWebhookUri(),
129
+            'webhookMethod' => $task->getWebhookMethod(),
130
+            'scheduledAt' => $task->getScheduledAt(),
131
+            'startedAt' => $task->getStartedAt(),
132
+            'endedAt' => $task->getEndedAt(),
133
+            'allowCleanup' => $task->getAllowCleanup() ? 1 : 0,
134
+            'userFacingErrorMessage' => $task->getUserFacingErrorMessage(),
135
+        ]);
136
+        return $taskEntity;
137
+    }
138 138
 
139
-	/**
140
-	 * @return OCPTask
141
-	 * @throws \JsonException
142
-	 */
143
-	public function toPublicTask(): OCPTask {
144
-		$task = new OCPTask($this->getType(), json_decode($this->getInput(), true, 512, JSON_THROW_ON_ERROR), $this->getAppId(), $this->getuserId(), $this->getCustomId());
145
-		$task->setId($this->getId());
146
-		$task->setStatus($this->getStatus());
147
-		$task->setLastUpdated($this->getLastUpdated());
148
-		$task->setOutput(json_decode($this->getOutput(), true, 512, JSON_THROW_ON_ERROR));
149
-		$task->setCompletionExpectedAt($this->getCompletionExpectedAt());
150
-		$task->setErrorMessage($this->getErrorMessage());
151
-		$task->setProgress($this->getProgress());
152
-		$task->setWebhookUri($this->getWebhookUri());
153
-		$task->setWebhookMethod($this->getWebhookMethod());
154
-		$task->setScheduledAt($this->getScheduledAt());
155
-		$task->setStartedAt($this->getStartedAt());
156
-		$task->setEndedAt($this->getEndedAt());
157
-		$task->setAllowCleanup($this->getAllowCleanup() !== 0);
158
-		$task->setUserFacingErrorMessage($this->getUserFacingErrorMessage());
159
-		return $task;
160
-	}
139
+    /**
140
+     * @return OCPTask
141
+     * @throws \JsonException
142
+     */
143
+    public function toPublicTask(): OCPTask {
144
+        $task = new OCPTask($this->getType(), json_decode($this->getInput(), true, 512, JSON_THROW_ON_ERROR), $this->getAppId(), $this->getuserId(), $this->getCustomId());
145
+        $task->setId($this->getId());
146
+        $task->setStatus($this->getStatus());
147
+        $task->setLastUpdated($this->getLastUpdated());
148
+        $task->setOutput(json_decode($this->getOutput(), true, 512, JSON_THROW_ON_ERROR));
149
+        $task->setCompletionExpectedAt($this->getCompletionExpectedAt());
150
+        $task->setErrorMessage($this->getErrorMessage());
151
+        $task->setProgress($this->getProgress());
152
+        $task->setWebhookUri($this->getWebhookUri());
153
+        $task->setWebhookMethod($this->getWebhookMethod());
154
+        $task->setScheduledAt($this->getScheduledAt());
155
+        $task->setStartedAt($this->getStartedAt());
156
+        $task->setEndedAt($this->getEndedAt());
157
+        $task->setAllowCleanup($this->getAllowCleanup() !== 0);
158
+        $task->setUserFacingErrorMessage($this->getUserFacingErrorMessage());
159
+        return $task;
160
+    }
161 161
 }
Please login to merge, or discard this patch.
lib/private/TaskProcessing/Manager.php 1 patch
Indentation   +1629 added lines, -1629 removed lines patch added patch discarded remove patch
@@ -73,1633 +73,1633 @@
 block discarded – undo
73 73
 
74 74
 class Manager implements IManager {
75 75
 
76
-	public const LEGACY_PREFIX_TEXTPROCESSING = 'legacy:TextProcessing:';
77
-	public const LEGACY_PREFIX_TEXTTOIMAGE = 'legacy:TextToImage:';
78
-	public const LEGACY_PREFIX_SPEECHTOTEXT = 'legacy:SpeechToText:';
79
-
80
-	public const LAZY_CONFIG_KEYS = [
81
-		'ai.taskprocessing_type_preferences',
82
-		'ai.taskprocessing_provider_preferences',
83
-	];
84
-
85
-	public const MAX_TASK_AGE_SECONDS = 60 * 60 * 24 * 31 * 6; // 6 months
86
-
87
-	private const TASK_TYPES_CACHE_KEY = 'available_task_types_v3';
88
-	private const TASK_TYPE_IDS_CACHE_KEY = 'available_task_type_ids';
89
-
90
-	/** @var list<IProvider>|null */
91
-	private ?array $providers = null;
92
-
93
-	/**
94
-	 * @var array<array-key,array{name: string, description: string, inputShape: ShapeDescriptor[], inputShapeEnumValues: ShapeEnumValue[][], inputShapeDefaults: array<array-key, numeric|string>, isInternal: bool, optionalInputShape: ShapeDescriptor[], optionalInputShapeEnumValues: ShapeEnumValue[][], optionalInputShapeDefaults: array<array-key, numeric|string>, outputShape: ShapeDescriptor[], outputShapeEnumValues: ShapeEnumValue[][], optionalOutputShape: ShapeDescriptor[], optionalOutputShapeEnumValues: ShapeEnumValue[][]}>
95
-	 */
96
-	private ?array $availableTaskTypes = null;
97
-
98
-	/** @var list<string>|null */
99
-	private ?array $availableTaskTypeIds = null;
100
-
101
-	private IAppData $appData;
102
-	private ?array $preferences = null;
103
-	private ?array $providersById = null;
104
-
105
-	/** @var ITaskType[]|null */
106
-	private ?array $taskTypes = null;
107
-	private ICache $distributedCache;
108
-
109
-	private ?GetTaskProcessingProvidersEvent $eventResult = null;
110
-
111
-	public function __construct(
112
-		private IAppConfig $appConfig,
113
-		private Coordinator $coordinator,
114
-		private IServerContainer $serverContainer,
115
-		private LoggerInterface $logger,
116
-		private TaskMapper $taskMapper,
117
-		private IJobList $jobList,
118
-		private IEventDispatcher $dispatcher,
119
-		IAppDataFactory $appDataFactory,
120
-		private IRootFolder $rootFolder,
121
-		private \OCP\TextToImage\IManager $textToImageManager,
122
-		private IUserMountCache $userMountCache,
123
-		private IClientService $clientService,
124
-		private IAppManager $appManager,
125
-		private IUserManager $userManager,
126
-		private IUserSession $userSession,
127
-		ICacheFactory $cacheFactory,
128
-		private IFactory $l10nFactory,
129
-	) {
130
-		$this->appData = $appDataFactory->get('core');
131
-		$this->distributedCache = $cacheFactory->createDistributed('task_processing::');
132
-	}
133
-
134
-
135
-	/**
136
-	 * This is almost a copy of textProcessingManager->getProviders
137
-	 * to avoid a dependency cycle between TextProcessingManager and TaskProcessingManager
138
-	 */
139
-	private function _getRawTextProcessingProviders(): array {
140
-		$context = $this->coordinator->getRegistrationContext();
141
-		if ($context === null) {
142
-			return [];
143
-		}
144
-
145
-		$providers = [];
146
-
147
-		foreach ($context->getTextProcessingProviders() as $providerServiceRegistration) {
148
-			$class = $providerServiceRegistration->getService();
149
-			try {
150
-				$providers[$class] = $this->serverContainer->get($class);
151
-			} catch (\Throwable $e) {
152
-				$this->logger->error('Failed to load Text processing provider ' . $class, [
153
-					'exception' => $e,
154
-				]);
155
-			}
156
-		}
157
-
158
-		return $providers;
159
-	}
160
-
161
-	private function _getTextProcessingProviders(): array {
162
-		$oldProviders = $this->_getRawTextProcessingProviders();
163
-		$newProviders = [];
164
-		foreach ($oldProviders as $oldProvider) {
165
-			$provider = new class($oldProvider) implements IProvider, ISynchronousProvider {
166
-				private \OCP\TextProcessing\IProvider $provider;
167
-
168
-				public function __construct(\OCP\TextProcessing\IProvider $provider) {
169
-					$this->provider = $provider;
170
-				}
171
-
172
-				public function getId(): string {
173
-					if ($this->provider instanceof \OCP\TextProcessing\IProviderWithId) {
174
-						return $this->provider->getId();
175
-					}
176
-					return Manager::LEGACY_PREFIX_TEXTPROCESSING . $this->provider::class;
177
-				}
178
-
179
-				public function getName(): string {
180
-					return $this->provider->getName();
181
-				}
182
-
183
-				public function getTaskTypeId(): string {
184
-					return match ($this->provider->getTaskType()) {
185
-						\OCP\TextProcessing\FreePromptTaskType::class => TextToText::ID,
186
-						\OCP\TextProcessing\HeadlineTaskType::class => TextToTextHeadline::ID,
187
-						\OCP\TextProcessing\TopicsTaskType::class => TextToTextTopics::ID,
188
-						\OCP\TextProcessing\SummaryTaskType::class => TextToTextSummary::ID,
189
-						default => Manager::LEGACY_PREFIX_TEXTPROCESSING . $this->provider->getTaskType(),
190
-					};
191
-				}
192
-
193
-				public function getExpectedRuntime(): int {
194
-					if ($this->provider instanceof \OCP\TextProcessing\IProviderWithExpectedRuntime) {
195
-						return $this->provider->getExpectedRuntime();
196
-					}
197
-					return 60;
198
-				}
199
-
200
-				public function getOptionalInputShape(): array {
201
-					return [];
202
-				}
203
-
204
-				public function getOptionalOutputShape(): array {
205
-					return [];
206
-				}
207
-
208
-				public function process(?string $userId, array $input, callable $reportProgress): array {
209
-					if ($this->provider instanceof \OCP\TextProcessing\IProviderWithUserId) {
210
-						$this->provider->setUserId($userId);
211
-					}
212
-					try {
213
-						return ['output' => $this->provider->process($input['input'])];
214
-					} catch (\RuntimeException $e) {
215
-						throw new ProcessingException($e->getMessage(), previous: $e);
216
-					}
217
-				}
218
-
219
-				public function getInputShapeEnumValues(): array {
220
-					return [];
221
-				}
222
-
223
-				public function getInputShapeDefaults(): array {
224
-					return [];
225
-				}
226
-
227
-				public function getOptionalInputShapeEnumValues(): array {
228
-					return [];
229
-				}
230
-
231
-				public function getOptionalInputShapeDefaults(): array {
232
-					return [];
233
-				}
234
-
235
-				public function getOutputShapeEnumValues(): array {
236
-					return [];
237
-				}
238
-
239
-				public function getOptionalOutputShapeEnumValues(): array {
240
-					return [];
241
-				}
242
-			};
243
-			$newProviders[$provider->getId()] = $provider;
244
-		}
245
-
246
-		return $newProviders;
247
-	}
248
-
249
-	/**
250
-	 * @return ITaskType[]
251
-	 */
252
-	private function _getTextProcessingTaskTypes(): array {
253
-		$oldProviders = $this->_getRawTextProcessingProviders();
254
-		$newTaskTypes = [];
255
-		foreach ($oldProviders as $oldProvider) {
256
-			// These are already implemented in the TaskProcessing realm
257
-			if (in_array($oldProvider->getTaskType(), [
258
-				\OCP\TextProcessing\FreePromptTaskType::class,
259
-				\OCP\TextProcessing\HeadlineTaskType::class,
260
-				\OCP\TextProcessing\TopicsTaskType::class,
261
-				\OCP\TextProcessing\SummaryTaskType::class
262
-			], true)) {
263
-				continue;
264
-			}
265
-			$taskType = new class($oldProvider->getTaskType()) implements ITaskType {
266
-				private string $oldTaskTypeClass;
267
-				private \OCP\TextProcessing\ITaskType $oldTaskType;
268
-				private IL10N $l;
269
-
270
-				public function __construct(string $oldTaskTypeClass) {
271
-					$this->oldTaskTypeClass = $oldTaskTypeClass;
272
-					$this->oldTaskType = \OCP\Server::get($oldTaskTypeClass);
273
-					$this->l = \OCP\Server::get(IFactory::class)->get('core');
274
-				}
275
-
276
-				public function getId(): string {
277
-					return Manager::LEGACY_PREFIX_TEXTPROCESSING . $this->oldTaskTypeClass;
278
-				}
279
-
280
-				public function getName(): string {
281
-					return $this->oldTaskType->getName();
282
-				}
283
-
284
-				public function getDescription(): string {
285
-					return $this->oldTaskType->getDescription();
286
-				}
287
-
288
-				public function getInputShape(): array {
289
-					return ['input' => new ShapeDescriptor($this->l->t('Input text'), $this->l->t('The input text'), EShapeType::Text)];
290
-				}
291
-
292
-				public function getOutputShape(): array {
293
-					return ['output' => new ShapeDescriptor($this->l->t('Input text'), $this->l->t('The input text'), EShapeType::Text)];
294
-				}
295
-			};
296
-			$newTaskTypes[$taskType->getId()] = $taskType;
297
-		}
298
-
299
-		return $newTaskTypes;
300
-	}
301
-
302
-	/**
303
-	 * @return IProvider[]
304
-	 */
305
-	private function _getTextToImageProviders(): array {
306
-		$oldProviders = $this->textToImageManager->getProviders();
307
-		$newProviders = [];
308
-		foreach ($oldProviders as $oldProvider) {
309
-			$newProvider = new class($oldProvider, $this->appData) implements IProvider, ISynchronousProvider {
310
-				private \OCP\TextToImage\IProvider $provider;
311
-				private IAppData $appData;
312
-
313
-				public function __construct(\OCP\TextToImage\IProvider $provider, IAppData $appData) {
314
-					$this->provider = $provider;
315
-					$this->appData = $appData;
316
-				}
317
-
318
-				public function getId(): string {
319
-					return Manager::LEGACY_PREFIX_TEXTTOIMAGE . $this->provider->getId();
320
-				}
321
-
322
-				public function getName(): string {
323
-					return $this->provider->getName();
324
-				}
325
-
326
-				public function getTaskTypeId(): string {
327
-					return TextToImage::ID;
328
-				}
329
-
330
-				public function getExpectedRuntime(): int {
331
-					return $this->provider->getExpectedRuntime();
332
-				}
333
-
334
-				public function getOptionalInputShape(): array {
335
-					return [];
336
-				}
337
-
338
-				public function getOptionalOutputShape(): array {
339
-					return [];
340
-				}
341
-
342
-				public function process(?string $userId, array $input, callable $reportProgress): array {
343
-					try {
344
-						$folder = $this->appData->getFolder('text2image');
345
-					} catch (\OCP\Files\NotFoundException) {
346
-						$folder = $this->appData->newFolder('text2image');
347
-					}
348
-					$resources = [];
349
-					$files = [];
350
-					for ($i = 0; $i < $input['numberOfImages']; $i++) {
351
-						$file = $folder->newFile(time() . '-' . rand(1, 100000) . '-' . $i);
352
-						$files[] = $file;
353
-						$resource = $file->write();
354
-						if ($resource !== false && $resource !== true && is_resource($resource)) {
355
-							$resources[] = $resource;
356
-						} else {
357
-							throw new ProcessingException('Text2Image generation using provider "' . $this->getName() . '" failed: Couldn\'t open file to write.');
358
-						}
359
-					}
360
-					if ($this->provider instanceof \OCP\TextToImage\IProviderWithUserId) {
361
-						$this->provider->setUserId($userId);
362
-					}
363
-					try {
364
-						$this->provider->generate($input['input'], $resources);
365
-					} catch (\RuntimeException $e) {
366
-						throw new ProcessingException($e->getMessage(), previous: $e);
367
-					}
368
-					for ($i = 0; $i < $input['numberOfImages']; $i++) {
369
-						if (is_resource($resources[$i])) {
370
-							// If $resource hasn't been closed yet, we'll do that here
371
-							fclose($resources[$i]);
372
-						}
373
-					}
374
-					return ['images' => array_map(fn (ISimpleFile $file) => $file->getContent(), $files)];
375
-				}
376
-
377
-				public function getInputShapeEnumValues(): array {
378
-					return [];
379
-				}
380
-
381
-				public function getInputShapeDefaults(): array {
382
-					return [];
383
-				}
384
-
385
-				public function getOptionalInputShapeEnumValues(): array {
386
-					return [];
387
-				}
388
-
389
-				public function getOptionalInputShapeDefaults(): array {
390
-					return [];
391
-				}
392
-
393
-				public function getOutputShapeEnumValues(): array {
394
-					return [];
395
-				}
396
-
397
-				public function getOptionalOutputShapeEnumValues(): array {
398
-					return [];
399
-				}
400
-			};
401
-			$newProviders[$newProvider->getId()] = $newProvider;
402
-		}
403
-
404
-		return $newProviders;
405
-	}
406
-
407
-	/**
408
-	 * This is almost a copy of SpeechToTextManager->getProviders
409
-	 * to avoid a dependency cycle between SpeechToTextManager and TaskProcessingManager
410
-	 */
411
-	private function _getRawSpeechToTextProviders(): array {
412
-		$context = $this->coordinator->getRegistrationContext();
413
-		if ($context === null) {
414
-			return [];
415
-		}
416
-		$providers = [];
417
-		foreach ($context->getSpeechToTextProviders() as $providerServiceRegistration) {
418
-			$class = $providerServiceRegistration->getService();
419
-			try {
420
-				$providers[$class] = $this->serverContainer->get($class);
421
-			} catch (NotFoundExceptionInterface|ContainerExceptionInterface|\Throwable $e) {
422
-				$this->logger->error('Failed to load SpeechToText provider ' . $class, [
423
-					'exception' => $e,
424
-				]);
425
-			}
426
-		}
427
-
428
-		return $providers;
429
-	}
430
-
431
-	/**
432
-	 * @return IProvider[]
433
-	 */
434
-	private function _getSpeechToTextProviders(): array {
435
-		$oldProviders = $this->_getRawSpeechToTextProviders();
436
-		$newProviders = [];
437
-		foreach ($oldProviders as $oldProvider) {
438
-			$newProvider = new class($oldProvider, $this->rootFolder, $this->appData) implements IProvider, ISynchronousProvider {
439
-				private ISpeechToTextProvider $provider;
440
-				private IAppData $appData;
441
-
442
-				private IRootFolder $rootFolder;
443
-
444
-				public function __construct(ISpeechToTextProvider $provider, IRootFolder $rootFolder, IAppData $appData) {
445
-					$this->provider = $provider;
446
-					$this->rootFolder = $rootFolder;
447
-					$this->appData = $appData;
448
-				}
449
-
450
-				public function getId(): string {
451
-					if ($this->provider instanceof ISpeechToTextProviderWithId) {
452
-						return Manager::LEGACY_PREFIX_SPEECHTOTEXT . $this->provider->getId();
453
-					}
454
-					return Manager::LEGACY_PREFIX_SPEECHTOTEXT . $this->provider::class;
455
-				}
456
-
457
-				public function getName(): string {
458
-					return $this->provider->getName();
459
-				}
460
-
461
-				public function getTaskTypeId(): string {
462
-					return AudioToText::ID;
463
-				}
464
-
465
-				public function getExpectedRuntime(): int {
466
-					return 60;
467
-				}
468
-
469
-				public function getOptionalInputShape(): array {
470
-					return [];
471
-				}
472
-
473
-				public function getOptionalOutputShape(): array {
474
-					return [];
475
-				}
476
-
477
-				public function process(?string $userId, array $input, callable $reportProgress): array {
478
-					if ($this->provider instanceof \OCP\SpeechToText\ISpeechToTextProviderWithUserId) {
479
-						$this->provider->setUserId($userId);
480
-					}
481
-					try {
482
-						$result = $this->provider->transcribeFile($input['input']);
483
-					} catch (\RuntimeException $e) {
484
-						throw new ProcessingException($e->getMessage(), previous: $e);
485
-					}
486
-					return ['output' => $result];
487
-				}
488
-
489
-				public function getInputShapeEnumValues(): array {
490
-					return [];
491
-				}
492
-
493
-				public function getInputShapeDefaults(): array {
494
-					return [];
495
-				}
496
-
497
-				public function getOptionalInputShapeEnumValues(): array {
498
-					return [];
499
-				}
500
-
501
-				public function getOptionalInputShapeDefaults(): array {
502
-					return [];
503
-				}
504
-
505
-				public function getOutputShapeEnumValues(): array {
506
-					return [];
507
-				}
508
-
509
-				public function getOptionalOutputShapeEnumValues(): array {
510
-					return [];
511
-				}
512
-			};
513
-			$newProviders[$newProvider->getId()] = $newProvider;
514
-		}
515
-
516
-		return $newProviders;
517
-	}
518
-
519
-	/**
520
-	 * Dispatches the event to collect external providers and task types.
521
-	 * Caches the result within the request.
522
-	 */
523
-	private function dispatchGetProvidersEvent(): GetTaskProcessingProvidersEvent {
524
-		if ($this->eventResult !== null) {
525
-			return $this->eventResult;
526
-		}
527
-
528
-		$this->eventResult = new GetTaskProcessingProvidersEvent();
529
-		$this->dispatcher->dispatchTyped($this->eventResult);
530
-		return $this->eventResult ;
531
-	}
532
-
533
-	/**
534
-	 * @return IProvider[]
535
-	 */
536
-	private function _getProviders(): array {
537
-		$context = $this->coordinator->getRegistrationContext();
538
-
539
-		if ($context === null) {
540
-			return [];
541
-		}
542
-
543
-		$providers = [];
544
-
545
-		foreach ($context->getTaskProcessingProviders() as $providerServiceRegistration) {
546
-			$class = $providerServiceRegistration->getService();
547
-			try {
548
-				/** @var IProvider $provider */
549
-				$provider = $this->serverContainer->get($class);
550
-				if (isset($providers[$provider->getId()])) {
551
-					$this->logger->warning('Task processing provider ' . $class . ' is using ID ' . $provider->getId() . ' which is already used by ' . $providers[$provider->getId()]::class);
552
-				}
553
-				$providers[$provider->getId()] = $provider;
554
-			} catch (\Throwable $e) {
555
-				$this->logger->error('Failed to load task processing provider ' . $class, [
556
-					'exception' => $e,
557
-				]);
558
-			}
559
-		}
560
-
561
-		$event = $this->dispatchGetProvidersEvent();
562
-		$externalProviders = $event->getProviders();
563
-		foreach ($externalProviders as $provider) {
564
-			if (!isset($providers[$provider->getId()])) {
565
-				$providers[$provider->getId()] = $provider;
566
-			} else {
567
-				$this->logger->info('Skipping external task processing provider with ID ' . $provider->getId() . ' because a local provider with the same ID already exists.');
568
-			}
569
-		}
570
-
571
-		$providers += $this->_getTextProcessingProviders() + $this->_getTextToImageProviders() + $this->_getSpeechToTextProviders();
572
-
573
-		return $providers;
574
-	}
575
-
576
-	/**
577
-	 * @return ITaskType[]
578
-	 */
579
-	private function _getTaskTypes(): array {
580
-		$context = $this->coordinator->getRegistrationContext();
581
-
582
-		if ($context === null) {
583
-			return [];
584
-		}
585
-
586
-		if ($this->taskTypes !== null) {
587
-			return $this->taskTypes;
588
-		}
589
-
590
-		// Default task types
591
-		$taskTypes = [
592
-			\OCP\TaskProcessing\TaskTypes\TextToText::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToText::class),
593
-			\OCP\TaskProcessing\TaskTypes\TextToTextTopics::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextTopics::class),
594
-			\OCP\TaskProcessing\TaskTypes\TextToTextHeadline::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextHeadline::class),
595
-			\OCP\TaskProcessing\TaskTypes\TextToTextSummary::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextSummary::class),
596
-			\OCP\TaskProcessing\TaskTypes\TextToTextFormalization::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextFormalization::class),
597
-			\OCP\TaskProcessing\TaskTypes\TextToTextSimplification::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextSimplification::class),
598
-			\OCP\TaskProcessing\TaskTypes\TextToTextChat::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextChat::class),
599
-			\OCP\TaskProcessing\TaskTypes\TextToTextTranslate::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextTranslate::class),
600
-			\OCP\TaskProcessing\TaskTypes\TextToTextReformulation::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextReformulation::class),
601
-			\OCP\TaskProcessing\TaskTypes\TextToImage::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToImage::class),
602
-			\OCP\TaskProcessing\TaskTypes\AudioToText::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\AudioToText::class),
603
-			\OCP\TaskProcessing\TaskTypes\ContextWrite::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\ContextWrite::class),
604
-			\OCP\TaskProcessing\TaskTypes\GenerateEmoji::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\GenerateEmoji::class),
605
-			\OCP\TaskProcessing\TaskTypes\TextToTextChangeTone::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextChangeTone::class),
606
-			\OCP\TaskProcessing\TaskTypes\TextToTextChatWithTools::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextChatWithTools::class),
607
-			\OCP\TaskProcessing\TaskTypes\ContextAgentInteraction::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\ContextAgentInteraction::class),
608
-			\OCP\TaskProcessing\TaskTypes\TextToTextProofread::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextProofread::class),
609
-			\OCP\TaskProcessing\TaskTypes\TextToSpeech::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToSpeech::class),
610
-			\OCP\TaskProcessing\TaskTypes\AudioToAudioChat::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\AudioToAudioChat::class),
611
-			\OCP\TaskProcessing\TaskTypes\ContextAgentAudioInteraction::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\ContextAgentAudioInteraction::class),
612
-			\OCP\TaskProcessing\TaskTypes\AnalyzeImages::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\AnalyzeImages::class),
613
-		];
614
-
615
-		foreach ($context->getTaskProcessingTaskTypes() as $providerServiceRegistration) {
616
-			$class = $providerServiceRegistration->getService();
617
-			try {
618
-				/** @var ITaskType $provider */
619
-				$taskType = $this->serverContainer->get($class);
620
-				if (isset($taskTypes[$taskType->getId()])) {
621
-					$this->logger->warning('Task processing task type ' . $class . ' is using ID ' . $taskType->getId() . ' which is already used by ' . $taskTypes[$taskType->getId()]::class);
622
-				}
623
-				$taskTypes[$taskType->getId()] = $taskType;
624
-			} catch (\Throwable $e) {
625
-				$this->logger->error('Failed to load task processing task type ' . $class, [
626
-					'exception' => $e,
627
-				]);
628
-			}
629
-		}
630
-
631
-		$event = $this->dispatchGetProvidersEvent();
632
-		$externalTaskTypes = $event->getTaskTypes();
633
-		foreach ($externalTaskTypes as $taskType) {
634
-			if (isset($taskTypes[$taskType->getId()])) {
635
-				$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()]) . ')');
636
-			}
637
-			$taskTypes[$taskType->getId()] = $taskType;
638
-		}
639
-
640
-		$taskTypes += $this->_getTextProcessingTaskTypes();
641
-
642
-		$this->taskTypes = $taskTypes;
643
-		return $this->taskTypes;
644
-	}
645
-
646
-	/**
647
-	 * @return array
648
-	 */
649
-	private function _getTaskTypeSettings(): array {
650
-		try {
651
-			$json = $this->appConfig->getValueString('core', 'ai.taskprocessing_type_preferences', '', lazy: true);
652
-			if ($json === '') {
653
-				return [];
654
-			}
655
-			return json_decode($json, true, flags: JSON_THROW_ON_ERROR);
656
-		} catch (\JsonException $e) {
657
-			$this->logger->error('Failed to get settings. JSON Error in ai.taskprocessing_type_preferences', ['exception' => $e]);
658
-			$taskTypeSettings = [];
659
-			$taskTypes = $this->_getTaskTypes();
660
-			foreach ($taskTypes as $taskType) {
661
-				$taskTypeSettings[$taskType->getId()] = false;
662
-			};
663
-
664
-			return $taskTypeSettings;
665
-		}
666
-
667
-	}
668
-
669
-	/**
670
-	 * @param ShapeDescriptor[] $spec
671
-	 * @param array<array-key, string|numeric> $defaults
672
-	 * @param array<array-key, ShapeEnumValue[]> $enumValues
673
-	 * @param array $io
674
-	 * @param bool $optional
675
-	 * @return void
676
-	 * @throws ValidationException
677
-	 */
678
-	private static function validateInput(array $spec, array $defaults, array $enumValues, array $io, bool $optional = false): void {
679
-		foreach ($spec as $key => $descriptor) {
680
-			$type = $descriptor->getShapeType();
681
-			if (!isset($io[$key])) {
682
-				if ($optional) {
683
-					continue;
684
-				}
685
-				if (isset($defaults[$key])) {
686
-					if (EShapeType::getScalarType($type) !== $type) {
687
-						throw new ValidationException('Provider tried to set a default value for a non-scalar slot');
688
-					}
689
-					if (EShapeType::isFileType($type)) {
690
-						throw new ValidationException('Provider tried to set a default value for a slot that is not text or number');
691
-					}
692
-					$type->validateInput($defaults[$key]);
693
-					continue;
694
-				}
695
-				throw new ValidationException('Missing key: "' . $key . '"');
696
-			}
697
-			try {
698
-				$type->validateInput($io[$key]);
699
-				if ($type === EShapeType::Enum) {
700
-					if (!isset($enumValues[$key])) {
701
-						throw new ValidationException('Provider did not provide enum values for an enum slot: "' . $key . '"');
702
-					}
703
-					$type->validateEnum($io[$key], $enumValues[$key]);
704
-				}
705
-			} catch (ValidationException $e) {
706
-				throw new ValidationException('Failed to validate input key "' . $key . '": ' . $e->getMessage());
707
-			}
708
-		}
709
-	}
710
-
711
-	/**
712
-	 * Takes task input data and replaces fileIds with File objects
713
-	 *
714
-	 * @param array<array-key, list<numeric|string>|numeric|string> $input
715
-	 * @param array<array-key, numeric|string> ...$defaultSpecs the specs
716
-	 * @return array<array-key, list<numeric|string>|numeric|string>
717
-	 */
718
-	public function fillInputDefaults(array $input, ...$defaultSpecs): array {
719
-		$spec = array_reduce($defaultSpecs, fn ($carry, $spec) => array_merge($carry, $spec), []);
720
-		return array_merge($spec, $input);
721
-	}
722
-
723
-	/**
724
-	 * @param ShapeDescriptor[] $spec
725
-	 * @param array<array-key, ShapeEnumValue[]> $enumValues
726
-	 * @param array $io
727
-	 * @param bool $optional
728
-	 * @return void
729
-	 * @throws ValidationException
730
-	 */
731
-	private static function validateOutputWithFileIds(array $spec, array $enumValues, array $io, bool $optional = false): void {
732
-		foreach ($spec as $key => $descriptor) {
733
-			$type = $descriptor->getShapeType();
734
-			if (!isset($io[$key])) {
735
-				if ($optional) {
736
-					continue;
737
-				}
738
-				throw new ValidationException('Missing key: "' . $key . '"');
739
-			}
740
-			try {
741
-				$type->validateOutputWithFileIds($io[$key]);
742
-				if (isset($enumValues[$key])) {
743
-					$type->validateEnum($io[$key], $enumValues[$key]);
744
-				}
745
-			} catch (ValidationException $e) {
746
-				throw new ValidationException('Failed to validate output key "' . $key . '": ' . $e->getMessage());
747
-			}
748
-		}
749
-	}
750
-
751
-	/**
752
-	 * @param ShapeDescriptor[] $spec
753
-	 * @param array<array-key, ShapeEnumValue[]> $enumValues
754
-	 * @param array $io
755
-	 * @param bool $optional
756
-	 * @return void
757
-	 * @throws ValidationException
758
-	 */
759
-	private static function validateOutputWithFileData(array $spec, array $enumValues, array $io, bool $optional = false): void {
760
-		foreach ($spec as $key => $descriptor) {
761
-			$type = $descriptor->getShapeType();
762
-			if (!isset($io[$key])) {
763
-				if ($optional) {
764
-					continue;
765
-				}
766
-				throw new ValidationException('Missing key: "' . $key . '"');
767
-			}
768
-			try {
769
-				$type->validateOutputWithFileData($io[$key]);
770
-				if (isset($enumValues[$key])) {
771
-					$type->validateEnum($io[$key], $enumValues[$key]);
772
-				}
773
-			} catch (ValidationException $e) {
774
-				throw new ValidationException('Failed to validate output key "' . $key . '": ' . $e->getMessage());
775
-			}
776
-		}
777
-	}
778
-
779
-	/**
780
-	 * @param array<array-key, T> $array The array to filter
781
-	 * @param ShapeDescriptor[] ...$specs the specs that define which keys to keep
782
-	 * @return array<array-key, T>
783
-	 * @psalm-template T
784
-	 */
785
-	private function removeSuperfluousArrayKeys(array $array, ...$specs): array {
786
-		$keys = array_unique(array_reduce($specs, fn ($carry, $spec) => array_merge($carry, array_keys($spec)), []));
787
-		$keys = array_filter($keys, fn ($key) => array_key_exists($key, $array));
788
-		$values = array_map(fn (string $key) => $array[$key], $keys);
789
-		return array_combine($keys, $values);
790
-	}
791
-
792
-	public function hasProviders(): bool {
793
-		return count($this->getProviders()) !== 0;
794
-	}
795
-
796
-	public function getProviders(): array {
797
-		if ($this->providers === null) {
798
-			$this->providers = $this->_getProviders();
799
-		}
800
-
801
-		return $this->providers;
802
-	}
803
-
804
-	public function getPreferredProvider(string $taskTypeId) {
805
-		try {
806
-			if ($this->preferences === null) {
807
-				$this->preferences = $this->distributedCache->get('ai.taskprocessing_provider_preferences');
808
-				if ($this->preferences === null) {
809
-					$this->preferences = json_decode(
810
-						$this->appConfig->getValueString('core', 'ai.taskprocessing_provider_preferences', 'null', lazy: true),
811
-						associative: true,
812
-						flags: JSON_THROW_ON_ERROR,
813
-					);
814
-					$this->distributedCache->set('ai.taskprocessing_provider_preferences', $this->preferences, 60 * 3);
815
-				}
816
-			}
817
-
818
-			$providers = $this->getProviders();
819
-			if (isset($this->preferences[$taskTypeId])) {
820
-				$providersById = $this->providersById ?? array_reduce($providers, static function (array $carry, IProvider $provider) {
821
-					$carry[$provider->getId()] = $provider;
822
-					return $carry;
823
-				}, []);
824
-				$this->providersById = $providersById;
825
-				if (isset($providersById[$this->preferences[$taskTypeId]])) {
826
-					return $providersById[$this->preferences[$taskTypeId]];
827
-				}
828
-			}
829
-			// By default, use the first available provider
830
-			foreach ($providers as $provider) {
831
-				if ($provider->getTaskTypeId() === $taskTypeId) {
832
-					return $provider;
833
-				}
834
-			}
835
-		} catch (\JsonException $e) {
836
-			$this->logger->warning('Failed to parse provider preferences while getting preferred provider for task type ' . $taskTypeId, ['exception' => $e]);
837
-		}
838
-		throw new \OCP\TaskProcessing\Exception\Exception('No matching provider found');
839
-	}
840
-
841
-	public function getAvailableTaskTypes(bool $showDisabled = false, ?string $userId = null): array {
842
-		// We cache by language, because some task type fields are translated
843
-		$cacheKey = self::TASK_TYPES_CACHE_KEY . ':' . $this->l10nFactory->findLanguage();
844
-
845
-		// userId will be obtained from the session if left to null
846
-		if (!$this->checkGuestAccess($userId)) {
847
-			return [];
848
-		}
849
-		if ($this->availableTaskTypes === null) {
850
-			$cachedValue = $this->distributedCache->get($cacheKey);
851
-			if ($cachedValue !== null) {
852
-				$this->availableTaskTypes = unserialize($cachedValue);
853
-			}
854
-		}
855
-		// Either we have no cache or showDisabled is turned on, which we don't want to cache, ever.
856
-		if ($this->availableTaskTypes === null || $showDisabled) {
857
-			$taskTypes = $this->_getTaskTypes();
858
-			$taskTypeSettings = $this->_getTaskTypeSettings();
859
-
860
-			$availableTaskTypes = [];
861
-			foreach ($taskTypes as $taskType) {
862
-				if ((!$showDisabled) && isset($taskTypeSettings[$taskType->getId()]) && !$taskTypeSettings[$taskType->getId()]) {
863
-					continue;
864
-				}
865
-				try {
866
-					$provider = $this->getPreferredProvider($taskType->getId());
867
-				} catch (\OCP\TaskProcessing\Exception\Exception $e) {
868
-					continue;
869
-				}
870
-				try {
871
-					$availableTaskTypes[$provider->getTaskTypeId()] = [
872
-						'name' => $taskType->getName(),
873
-						'description' => $taskType->getDescription(),
874
-						'optionalInputShape' => $provider->getOptionalInputShape(),
875
-						'inputShapeEnumValues' => $provider->getInputShapeEnumValues(),
876
-						'inputShapeDefaults' => $provider->getInputShapeDefaults(),
877
-						'inputShape' => $taskType->getInputShape(),
878
-						'optionalInputShapeEnumValues' => $provider->getOptionalInputShapeEnumValues(),
879
-						'optionalInputShapeDefaults' => $provider->getOptionalInputShapeDefaults(),
880
-						'outputShape' => $taskType->getOutputShape(),
881
-						'outputShapeEnumValues' => $provider->getOutputShapeEnumValues(),
882
-						'optionalOutputShape' => $provider->getOptionalOutputShape(),
883
-						'optionalOutputShapeEnumValues' => $provider->getOptionalOutputShapeEnumValues(),
884
-						'isInternal' => $taskType instanceof IInternalTaskType,
885
-					];
886
-				} catch (\Throwable $e) {
887
-					$this->logger->error('Failed to set up TaskProcessing provider ' . $provider::class, ['exception' => $e]);
888
-				}
889
-			}
890
-
891
-			if ($showDisabled) {
892
-				// Do not cache showDisabled, ever.
893
-				return $availableTaskTypes;
894
-			}
895
-
896
-			$this->availableTaskTypes = $availableTaskTypes;
897
-			$this->distributedCache->set($cacheKey, serialize($this->availableTaskTypes), 60);
898
-		}
899
-
900
-
901
-		return $this->availableTaskTypes;
902
-	}
903
-	public function getAvailableTaskTypeIds(bool $showDisabled = false, ?string $userId = null): array {
904
-		// userId will be obtained from the session if left to null
905
-		if (!$this->checkGuestAccess($userId)) {
906
-			return [];
907
-		}
908
-		if ($this->availableTaskTypeIds === null) {
909
-			$cachedValue = $this->distributedCache->get(self::TASK_TYPE_IDS_CACHE_KEY);
910
-			if ($cachedValue !== null) {
911
-				$this->availableTaskTypeIds = $cachedValue;
912
-			}
913
-		}
914
-		// Either we have no cache or showDisabled is turned on, which we don't want to cache, ever.
915
-		if ($this->availableTaskTypeIds === null || $showDisabled) {
916
-			$taskTypes = $this->_getTaskTypes();
917
-			$taskTypeSettings = $this->_getTaskTypeSettings();
918
-
919
-			$availableTaskTypeIds = [];
920
-			foreach ($taskTypes as $taskType) {
921
-				if ((!$showDisabled) && isset($taskTypeSettings[$taskType->getId()]) && !$taskTypeSettings[$taskType->getId()]) {
922
-					continue;
923
-				}
924
-				try {
925
-					$provider = $this->getPreferredProvider($taskType->getId());
926
-				} catch (\OCP\TaskProcessing\Exception\Exception $e) {
927
-					continue;
928
-				}
929
-				$availableTaskTypeIds[] = $taskType->getId();
930
-			}
931
-
932
-			if ($showDisabled) {
933
-				// Do not cache showDisabled, ever.
934
-				return $availableTaskTypeIds;
935
-			}
936
-
937
-			$this->availableTaskTypeIds = $availableTaskTypeIds;
938
-			$this->distributedCache->set(self::TASK_TYPE_IDS_CACHE_KEY, $this->availableTaskTypeIds, 60);
939
-		}
940
-
941
-
942
-		return $this->availableTaskTypeIds;
943
-	}
944
-
945
-	public function canHandleTask(Task $task): bool {
946
-		return isset($this->getAvailableTaskTypes()[$task->getTaskTypeId()]);
947
-	}
948
-
949
-	private function checkGuestAccess(?string $userId = null): bool {
950
-		if ($userId === null && !$this->userSession->isLoggedIn()) {
951
-			return true;
952
-		}
953
-		if ($userId === null) {
954
-			$user = $this->userSession->getUser();
955
-		} else {
956
-			$user = $this->userManager->get($userId);
957
-		}
958
-
959
-		$guestsAllowed = $this->appConfig->getValueString('core', 'ai.taskprocessing_guests', 'false');
960
-		if ($guestsAllowed == 'true' || !class_exists(\OCA\Guests\UserBackend::class) || !($user->getBackend() instanceof \OCA\Guests\UserBackend)) {
961
-			return true;
962
-		}
963
-		return false;
964
-	}
965
-
966
-	public function scheduleTask(Task $task): void {
967
-		if (!$this->checkGuestAccess($task->getUserId())) {
968
-			throw new \OCP\TaskProcessing\Exception\PreConditionNotMetException('Access to this resource is forbidden for guests.');
969
-		}
970
-		if (!$this->canHandleTask($task)) {
971
-			throw new \OCP\TaskProcessing\Exception\PreConditionNotMetException('No task processing provider is installed that can handle this task type: ' . $task->getTaskTypeId());
972
-		}
973
-		$this->prepareTask($task);
974
-		$task->setStatus(Task::STATUS_SCHEDULED);
975
-		$this->storeTask($task);
976
-		// schedule synchronous job if the provider is synchronous
977
-		$provider = $this->getPreferredProvider($task->getTaskTypeId());
978
-		if ($provider instanceof ISynchronousProvider) {
979
-			$this->jobList->add(SynchronousBackgroundJob::class, null);
980
-		}
981
-		if ($provider instanceof ITriggerableProvider) {
982
-			try {
983
-				if (!$this->taskMapper->hasRunningTasksForTaskType($task->getTaskTypeId())) {
984
-					// If no tasks are currently running for this task type, nudge the provider to ask for tasks
985
-					try {
986
-						$provider->trigger();
987
-					} catch (\Throwable $e) {
988
-						$this->logger->error('Failed to trigger the provider after scheduling a task.', [
989
-							'exception' => $e,
990
-							'taskId' => $task->getId(),
991
-							'providerId' => $provider->getId(),
992
-						]);
993
-					}
994
-				}
995
-			} catch (Exception $e) {
996
-				$this->logger->error('Failed to check DB for running tasks after a task was scheduled for a triggerable provider. Not triggering the provider.', [
997
-					'exception' => $e,
998
-					'taskId' => $task->getId(),
999
-					'providerId' => $provider->getId()
1000
-				]);
1001
-			}
1002
-		}
1003
-	}
1004
-
1005
-	public function runTask(Task $task): Task {
1006
-		if (!$this->checkGuestAccess($task->getUserId())) {
1007
-			throw new \OCP\TaskProcessing\Exception\PreConditionNotMetException('Access to this resource is forbidden for guests.');
1008
-		}
1009
-		if (!$this->canHandleTask($task)) {
1010
-			throw new \OCP\TaskProcessing\Exception\PreConditionNotMetException('No task processing provider is installed that can handle this task type: ' . $task->getTaskTypeId());
1011
-		}
1012
-
1013
-		$provider = $this->getPreferredProvider($task->getTaskTypeId());
1014
-		if ($provider instanceof ISynchronousProvider) {
1015
-			$this->prepareTask($task);
1016
-			$task->setStatus(Task::STATUS_SCHEDULED);
1017
-			$this->storeTask($task);
1018
-			$this->processTask($task, $provider);
1019
-			$task = $this->getTask($task->getId());
1020
-		} else {
1021
-			$this->scheduleTask($task);
1022
-			// poll task
1023
-			while ($task->getStatus() === Task::STATUS_SCHEDULED || $task->getStatus() === Task::STATUS_RUNNING) {
1024
-				sleep(1);
1025
-				$task = $this->getTask($task->getId());
1026
-			}
1027
-		}
1028
-		return $task;
1029
-	}
1030
-
1031
-	public function processTask(Task $task, ISynchronousProvider $provider): bool {
1032
-		try {
1033
-			try {
1034
-				$input = $this->prepareInputData($task);
1035
-			} catch (GenericFileException|NotPermittedException|LockedException|ValidationException|UnauthorizedException $e) {
1036
-				$this->logger->warning('Failed to prepare input data for a TaskProcessing task with synchronous provider ' . $provider->getId(), ['exception' => $e]);
1037
-				$this->setTaskResult($task->getId(), $e->getMessage(), null);
1038
-				return false;
1039
-			}
1040
-			try {
1041
-				$this->setTaskStatus($task, Task::STATUS_RUNNING);
1042
-				$output = $provider->process($task->getUserId(), $input, fn (float $progress) => $this->setTaskProgress($task->getId(), $progress));
1043
-			} catch (ProcessingException $e) {
1044
-				$this->logger->warning('Failed to process a TaskProcessing task with synchronous provider ' . $provider->getId(), ['exception' => $e]);
1045
-				$userFacingErrorMessage = $e instanceof UserFacingProcessingException ? $e->getUserFacingMessage() : null;
1046
-				$this->setTaskResult($task->getId(), $e->getMessage(), null, userFacingError: $userFacingErrorMessage);
1047
-				return false;
1048
-			} catch (\Throwable $e) {
1049
-				$this->logger->error('Unknown error while processing TaskProcessing task', ['exception' => $e]);
1050
-				$this->setTaskResult($task->getId(), $e->getMessage(), null);
1051
-				return false;
1052
-			}
1053
-			$this->setTaskResult($task->getId(), null, $output);
1054
-		} catch (NotFoundException $e) {
1055
-			$this->logger->info('Could not find task anymore after execution. Moving on.', ['exception' => $e]);
1056
-		} catch (Exception $e) {
1057
-			$this->logger->error('Failed to report result of TaskProcessing task', ['exception' => $e]);
1058
-		}
1059
-		return true;
1060
-	}
1061
-
1062
-	public function deleteTask(Task $task): void {
1063
-		$taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
1064
-		$this->taskMapper->delete($taskEntity);
1065
-	}
1066
-
1067
-	public function getTask(int $id): Task {
1068
-		try {
1069
-			$taskEntity = $this->taskMapper->find($id);
1070
-			return $taskEntity->toPublicTask();
1071
-		} catch (DoesNotExistException $e) {
1072
-			throw new NotFoundException('Couldn\'t find task with id ' . $id, 0, $e);
1073
-		} catch (MultipleObjectsReturnedException|\OCP\DB\Exception $e) {
1074
-			throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding the task', 0, $e);
1075
-		} catch (\JsonException $e) {
1076
-			throw new \OCP\TaskProcessing\Exception\Exception('There was a problem parsing JSON after finding the task', 0, $e);
1077
-		}
1078
-	}
1079
-
1080
-	public function cancelTask(int $id): void {
1081
-		$task = $this->getTask($id);
1082
-		if ($task->getStatus() !== Task::STATUS_SCHEDULED && $task->getStatus() !== Task::STATUS_RUNNING) {
1083
-			return;
1084
-		}
1085
-		$task->setStatus(Task::STATUS_CANCELLED);
1086
-		$task->setEndedAt(time());
1087
-		$taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
1088
-		try {
1089
-			$this->taskMapper->update($taskEntity);
1090
-			$this->runWebhook($task);
1091
-		} catch (\OCP\DB\Exception $e) {
1092
-			throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding the task', 0, $e);
1093
-		}
1094
-	}
1095
-
1096
-	public function setTaskProgress(int $id, float $progress): bool {
1097
-		// TODO: Not sure if we should rather catch the exceptions of getTask here and fail silently
1098
-		$task = $this->getTask($id);
1099
-		if ($task->getStatus() === Task::STATUS_CANCELLED) {
1100
-			return false;
1101
-		}
1102
-		// only set the start time if the task is going from scheduled to running
1103
-		if ($task->getstatus() === Task::STATUS_SCHEDULED) {
1104
-			$task->setStartedAt(time());
1105
-		}
1106
-		$task->setStatus(Task::STATUS_RUNNING);
1107
-		$task->setProgress($progress);
1108
-		$taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
1109
-		try {
1110
-			$this->taskMapper->update($taskEntity);
1111
-		} catch (\OCP\DB\Exception $e) {
1112
-			throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding the task', 0, $e);
1113
-		}
1114
-		return true;
1115
-	}
1116
-
1117
-	public function setTaskResult(int $id, ?string $error, ?array $result, bool $isUsingFileIds = false, ?string $userFacingError = null): void {
1118
-		// TODO: Not sure if we should rather catch the exceptions of getTask here and fail silently
1119
-		$task = $this->getTask($id);
1120
-		if ($task->getStatus() === Task::STATUS_CANCELLED) {
1121
-			$this->logger->info('A TaskProcessing ' . $task->getTaskTypeId() . ' task with id ' . $id . ' finished but was cancelled in the mean time. Moving on without storing result.');
1122
-			return;
1123
-		}
1124
-		if ($error !== null) {
1125
-			$task->setStatus(Task::STATUS_FAILED);
1126
-			$task->setEndedAt(time());
1127
-			// truncate error message to 4000 characters
1128
-			$task->setErrorMessage(substr($error, 0, 4000));
1129
-			// truncate error message to 4000 characters
1130
-			if ($userFacingError !== null) {
1131
-				$task->setUserFacingErrorMessage(substr($userFacingError, 0, 4000));
1132
-			}
1133
-			$this->logger->warning('A TaskProcessing ' . $task->getTaskTypeId() . ' task with id ' . $id . ' failed with the following message: ' . $error);
1134
-		} elseif ($result !== null) {
1135
-			$taskTypes = $this->getAvailableTaskTypes();
1136
-			$outputShape = $taskTypes[$task->getTaskTypeId()]['outputShape'];
1137
-			$outputShapeEnumValues = $taskTypes[$task->getTaskTypeId()]['outputShapeEnumValues'];
1138
-			$optionalOutputShape = $taskTypes[$task->getTaskTypeId()]['optionalOutputShape'];
1139
-			$optionalOutputShapeEnumValues = $taskTypes[$task->getTaskTypeId()]['optionalOutputShapeEnumValues'];
1140
-			try {
1141
-				// validate output
1142
-				if (!$isUsingFileIds) {
1143
-					$this->validateOutputWithFileData($outputShape, $outputShapeEnumValues, $result);
1144
-					$this->validateOutputWithFileData($optionalOutputShape, $optionalOutputShapeEnumValues, $result, true);
1145
-				} else {
1146
-					$this->validateOutputWithFileIds($outputShape, $outputShapeEnumValues, $result);
1147
-					$this->validateOutputWithFileIds($optionalOutputShape, $optionalOutputShapeEnumValues, $result, true);
1148
-				}
1149
-				$output = $this->removeSuperfluousArrayKeys($result, $outputShape, $optionalOutputShape);
1150
-				// extract raw data and put it in files, replace it with file ids
1151
-				if (!$isUsingFileIds) {
1152
-					$output = $this->encapsulateOutputFileData($output, $outputShape, $optionalOutputShape);
1153
-				} else {
1154
-					$this->validateOutputFileIds($output, $outputShape, $optionalOutputShape);
1155
-				}
1156
-				// Turn file objects into IDs
1157
-				foreach ($output as $key => $value) {
1158
-					if ($value instanceof Node) {
1159
-						$output[$key] = $value->getId();
1160
-					}
1161
-					if (is_array($value) && isset($value[0]) && $value[0] instanceof Node) {
1162
-						$output[$key] = array_map(fn ($node) => $node->getId(), $value);
1163
-					}
1164
-				}
1165
-				$task->setOutput($output);
1166
-				$task->setProgress(1);
1167
-				$task->setStatus(Task::STATUS_SUCCESSFUL);
1168
-				$task->setEndedAt(time());
1169
-			} catch (ValidationException $e) {
1170
-				$task->setProgress(1);
1171
-				$task->setStatus(Task::STATUS_FAILED);
1172
-				$task->setEndedAt(time());
1173
-				$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';
1174
-				$task->setErrorMessage($error);
1175
-				$this->logger->error($error, ['exception' => $e, 'output' => $result]);
1176
-			} catch (NotPermittedException $e) {
1177
-				$task->setProgress(1);
1178
-				$task->setStatus(Task::STATUS_FAILED);
1179
-				$task->setEndedAt(time());
1180
-				$error = 'The task was processed successfully but storing the output in a file failed';
1181
-				$task->setErrorMessage($error);
1182
-				$this->logger->error($error, ['exception' => $e]);
1183
-			} catch (InvalidPathException|\OCP\Files\NotFoundException $e) {
1184
-				$task->setProgress(1);
1185
-				$task->setStatus(Task::STATUS_FAILED);
1186
-				$task->setEndedAt(time());
1187
-				$error = 'The task was processed successfully but the result file could not be found';
1188
-				$task->setErrorMessage($error);
1189
-				$this->logger->error($error, ['exception' => $e]);
1190
-			}
1191
-		}
1192
-		try {
1193
-			$taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
1194
-		} catch (\JsonException $e) {
1195
-			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);
1196
-		}
1197
-		try {
1198
-			$this->taskMapper->update($taskEntity);
1199
-			$this->runWebhook($task);
1200
-		} catch (\OCP\DB\Exception $e) {
1201
-			throw new \OCP\TaskProcessing\Exception\Exception($e->getMessage());
1202
-		}
1203
-		if ($task->getStatus() === Task::STATUS_SUCCESSFUL) {
1204
-			$event = new TaskSuccessfulEvent($task);
1205
-		} else {
1206
-			$event = new TaskFailedEvent($task, $error);
1207
-		}
1208
-		$this->dispatcher->dispatchTyped($event);
1209
-	}
1210
-
1211
-	public function getNextScheduledTask(array $taskTypeIds = [], array $taskIdsToIgnore = []): Task {
1212
-		try {
1213
-			$taskEntity = $this->taskMapper->findOldestScheduledByType($taskTypeIds, $taskIdsToIgnore);
1214
-			return $taskEntity->toPublicTask();
1215
-		} catch (DoesNotExistException $e) {
1216
-			throw new \OCP\TaskProcessing\Exception\NotFoundException('Could not find the task', previous: $e);
1217
-		} catch (\OCP\DB\Exception $e) {
1218
-			throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding the task', previous: $e);
1219
-		} catch (\JsonException $e) {
1220
-			throw new \OCP\TaskProcessing\Exception\Exception('There was a problem parsing JSON after finding the task', previous: $e);
1221
-		}
1222
-	}
1223
-
1224
-	public function getNextScheduledTasks(array $taskTypeIds = [], array $taskIdsToIgnore = [], int $numberOfTasks = 1): array {
1225
-		try {
1226
-			return array_map(fn ($taskEntity) => $taskEntity->toPublicTask(), $this->taskMapper->findNOldestScheduledByType($taskTypeIds, $taskIdsToIgnore, $numberOfTasks));
1227
-		} catch (DoesNotExistException $e) {
1228
-			throw new \OCP\TaskProcessing\Exception\NotFoundException('Could not find the task', previous: $e);
1229
-		} catch (\OCP\DB\Exception $e) {
1230
-			throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding the task', previous: $e);
1231
-		} catch (\JsonException $e) {
1232
-			throw new \OCP\TaskProcessing\Exception\Exception('There was a problem parsing JSON after finding the task', previous: $e);
1233
-		}
1234
-	}
1235
-
1236
-	/**
1237
-	 * Takes task input data and replaces fileIds with File objects
1238
-	 *
1239
-	 * @param string|null $userId
1240
-	 * @param array<array-key, list<numeric|string>|numeric|string> $input
1241
-	 * @param ShapeDescriptor[] ...$specs the specs
1242
-	 * @return array<array-key, list<File|numeric|string>|numeric|string|File>
1243
-	 * @throws GenericFileException|LockedException|NotPermittedException|ValidationException|UnauthorizedException
1244
-	 */
1245
-	public function fillInputFileData(?string $userId, array $input, ...$specs): array {
1246
-		if ($userId !== null) {
1247
-			\OC_Util::setupFS($userId);
1248
-		}
1249
-		$newInputOutput = [];
1250
-		$spec = array_reduce($specs, fn ($carry, $spec) => $carry + $spec, []);
1251
-		foreach ($spec as $key => $descriptor) {
1252
-			$type = $descriptor->getShapeType();
1253
-			if (!isset($input[$key])) {
1254
-				continue;
1255
-			}
1256
-			if (!in_array(EShapeType::getScalarType($type), [EShapeType::Image, EShapeType::Audio, EShapeType::Video, EShapeType::File], true)) {
1257
-				$newInputOutput[$key] = $input[$key];
1258
-				continue;
1259
-			}
1260
-			if (EShapeType::getScalarType($type) === $type) {
1261
-				// is scalar
1262
-				$node = $this->validateFileId((int)$input[$key]);
1263
-				$this->validateUserAccessToFile($input[$key], $userId);
1264
-				$newInputOutput[$key] = $node;
1265
-			} else {
1266
-				// is list
1267
-				$newInputOutput[$key] = [];
1268
-				foreach ($input[$key] as $item) {
1269
-					$node = $this->validateFileId((int)$item);
1270
-					$this->validateUserAccessToFile($item, $userId);
1271
-					$newInputOutput[$key][] = $node;
1272
-				}
1273
-			}
1274
-		}
1275
-		return $newInputOutput;
1276
-	}
1277
-
1278
-	public function getUserTask(int $id, ?string $userId): Task {
1279
-		try {
1280
-			$taskEntity = $this->taskMapper->findByIdAndUser($id, $userId);
1281
-			return $taskEntity->toPublicTask();
1282
-		} catch (DoesNotExistException $e) {
1283
-			throw new \OCP\TaskProcessing\Exception\NotFoundException('Could not find the task', 0, $e);
1284
-		} catch (MultipleObjectsReturnedException|\OCP\DB\Exception $e) {
1285
-			throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding the task', 0, $e);
1286
-		} catch (\JsonException $e) {
1287
-			throw new \OCP\TaskProcessing\Exception\Exception('There was a problem parsing JSON after finding the task', 0, $e);
1288
-		}
1289
-	}
1290
-
1291
-	public function getUserTasks(?string $userId, ?string $taskTypeId = null, ?string $customId = null): array {
1292
-		try {
1293
-			$taskEntities = $this->taskMapper->findByUserAndTaskType($userId, $taskTypeId, $customId);
1294
-			return array_map(fn ($taskEntity): Task => $taskEntity->toPublicTask(), $taskEntities);
1295
-		} catch (\OCP\DB\Exception $e) {
1296
-			throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding the tasks', 0, $e);
1297
-		} catch (\JsonException $e) {
1298
-			throw new \OCP\TaskProcessing\Exception\Exception('There was a problem parsing JSON after finding the tasks', 0, $e);
1299
-		}
1300
-	}
1301
-
1302
-	public function getTasks(
1303
-		?string $userId, ?string $taskTypeId = null, ?string $appId = null, ?string $customId = null,
1304
-		?int $status = null, ?int $scheduleAfter = null, ?int $endedBefore = null,
1305
-	): array {
1306
-		try {
1307
-			$taskEntities = $this->taskMapper->findTasks($userId, $taskTypeId, $appId, $customId, $status, $scheduleAfter, $endedBefore);
1308
-			return array_map(fn ($taskEntity): Task => $taskEntity->toPublicTask(), $taskEntities);
1309
-		} catch (\OCP\DB\Exception $e) {
1310
-			throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding the tasks', 0, $e);
1311
-		} catch (\JsonException $e) {
1312
-			throw new \OCP\TaskProcessing\Exception\Exception('There was a problem parsing JSON after finding the tasks', 0, $e);
1313
-		}
1314
-	}
1315
-
1316
-	public function getUserTasksByApp(?string $userId, string $appId, ?string $customId = null): array {
1317
-		try {
1318
-			$taskEntities = $this->taskMapper->findUserTasksByApp($userId, $appId, $customId);
1319
-			return array_map(fn ($taskEntity): Task => $taskEntity->toPublicTask(), $taskEntities);
1320
-		} catch (\OCP\DB\Exception $e) {
1321
-			throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding a task', 0, $e);
1322
-		} catch (\JsonException $e) {
1323
-			throw new \OCP\TaskProcessing\Exception\Exception('There was a problem parsing JSON after finding a task', 0, $e);
1324
-		}
1325
-	}
1326
-
1327
-	/**
1328
-	 *Takes task input or output and replaces base64 data with file ids
1329
-	 *
1330
-	 * @param array $output
1331
-	 * @param ShapeDescriptor[] ...$specs the specs that define which keys to keep
1332
-	 * @return array
1333
-	 * @throws NotPermittedException
1334
-	 */
1335
-	public function encapsulateOutputFileData(array $output, ...$specs): array {
1336
-		$newOutput = [];
1337
-		try {
1338
-			$folder = $this->appData->getFolder('TaskProcessing');
1339
-		} catch (\OCP\Files\NotFoundException) {
1340
-			$folder = $this->appData->newFolder('TaskProcessing');
1341
-		}
1342
-		$spec = array_reduce($specs, fn ($carry, $spec) => $carry + $spec, []);
1343
-		foreach ($spec as $key => $descriptor) {
1344
-			$type = $descriptor->getShapeType();
1345
-			if (!isset($output[$key])) {
1346
-				continue;
1347
-			}
1348
-			if (!in_array(EShapeType::getScalarType($type), [EShapeType::Image, EShapeType::Audio, EShapeType::Video, EShapeType::File], true)) {
1349
-				$newOutput[$key] = $output[$key];
1350
-				continue;
1351
-			}
1352
-			if (EShapeType::getScalarType($type) === $type) {
1353
-				/** @var SimpleFile $file */
1354
-				$file = $folder->newFile(time() . '-' . rand(1, 100000), $output[$key]);
1355
-				$newOutput[$key] = $file->getId(); // polymorphic call to SimpleFile
1356
-			} else {
1357
-				$newOutput = [];
1358
-				foreach ($output[$key] as $item) {
1359
-					/** @var SimpleFile $file */
1360
-					$file = $folder->newFile(time() . '-' . rand(1, 100000), $item);
1361
-					$newOutput[$key][] = $file->getId();
1362
-				}
1363
-			}
1364
-		}
1365
-		return $newOutput;
1366
-	}
1367
-
1368
-	/**
1369
-	 * @param Task $task
1370
-	 * @return array<array-key, list<numeric|string|File>|numeric|string|File>
1371
-	 * @throws GenericFileException
1372
-	 * @throws LockedException
1373
-	 * @throws NotPermittedException
1374
-	 * @throws ValidationException|UnauthorizedException
1375
-	 */
1376
-	public function prepareInputData(Task $task): array {
1377
-		$taskTypes = $this->getAvailableTaskTypes();
1378
-		$inputShape = $taskTypes[$task->getTaskTypeId()]['inputShape'];
1379
-		$optionalInputShape = $taskTypes[$task->getTaskTypeId()]['optionalInputShape'];
1380
-		$input = $task->getInput();
1381
-		$input = $this->removeSuperfluousArrayKeys($input, $inputShape, $optionalInputShape);
1382
-		$input = $this->fillInputFileData($task->getUserId(), $input, $inputShape, $optionalInputShape);
1383
-		return $input;
1384
-	}
1385
-
1386
-	public function lockTask(Task $task): bool {
1387
-		$taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
1388
-		if ($this->taskMapper->lockTask($taskEntity) === 0) {
1389
-			return false;
1390
-		}
1391
-		$task->setStatus(Task::STATUS_RUNNING);
1392
-		return true;
1393
-	}
1394
-
1395
-	/**
1396
-	 * @throws \JsonException
1397
-	 * @throws Exception
1398
-	 */
1399
-	public function setTaskStatus(Task $task, int $status): void {
1400
-		$currentTaskStatus = $task->getStatus();
1401
-		if ($currentTaskStatus === Task::STATUS_SCHEDULED && $status === Task::STATUS_RUNNING) {
1402
-			$task->setStartedAt(time());
1403
-		} elseif ($currentTaskStatus === Task::STATUS_RUNNING && ($status === Task::STATUS_FAILED || $status === Task::STATUS_CANCELLED)) {
1404
-			$task->setEndedAt(time());
1405
-		} elseif ($currentTaskStatus === Task::STATUS_UNKNOWN && $status === Task::STATUS_SCHEDULED) {
1406
-			$task->setScheduledAt(time());
1407
-		}
1408
-		$task->setStatus($status);
1409
-		$taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
1410
-		$this->taskMapper->update($taskEntity);
1411
-	}
1412
-
1413
-	/**
1414
-	 * Validate input, fill input default values, set completionExpectedAt, set scheduledAt
1415
-	 *
1416
-	 * @param Task $task
1417
-	 * @return void
1418
-	 * @throws UnauthorizedException
1419
-	 * @throws ValidationException
1420
-	 * @throws \OCP\TaskProcessing\Exception\Exception
1421
-	 */
1422
-	private function prepareTask(Task $task): void {
1423
-		$taskTypes = $this->getAvailableTaskTypes();
1424
-		$taskType = $taskTypes[$task->getTaskTypeId()];
1425
-		$inputShape = $taskType['inputShape'];
1426
-		$inputShapeDefaults = $taskType['inputShapeDefaults'];
1427
-		$inputShapeEnumValues = $taskType['inputShapeEnumValues'];
1428
-		$optionalInputShape = $taskType['optionalInputShape'];
1429
-		$optionalInputShapeEnumValues = $taskType['optionalInputShapeEnumValues'];
1430
-		$optionalInputShapeDefaults = $taskType['optionalInputShapeDefaults'];
1431
-		// validate input
1432
-		$this->validateInput($inputShape, $inputShapeDefaults, $inputShapeEnumValues, $task->getInput());
1433
-		$this->validateInput($optionalInputShape, $optionalInputShapeDefaults, $optionalInputShapeEnumValues, $task->getInput(), true);
1434
-		// authenticate access to mentioned files
1435
-		$ids = [];
1436
-		foreach ($inputShape + $optionalInputShape as $key => $descriptor) {
1437
-			if (in_array(EShapeType::getScalarType($descriptor->getShapeType()), [EShapeType::File, EShapeType::Image, EShapeType::Audio, EShapeType::Video], true)) {
1438
-				/** @var list<int>|int $inputSlot */
1439
-				$inputSlot = $task->getInput()[$key];
1440
-				if (is_array($inputSlot)) {
1441
-					$ids += $inputSlot;
1442
-				} else {
1443
-					$ids[] = $inputSlot;
1444
-				}
1445
-			}
1446
-		}
1447
-		foreach ($ids as $fileId) {
1448
-			$this->validateFileId($fileId);
1449
-			$this->validateUserAccessToFile($fileId, $task->getUserId());
1450
-		}
1451
-		// remove superfluous keys and set input
1452
-		$input = $this->removeSuperfluousArrayKeys($task->getInput(), $inputShape, $optionalInputShape);
1453
-		$inputWithDefaults = $this->fillInputDefaults($input, $inputShapeDefaults, $optionalInputShapeDefaults);
1454
-		$task->setInput($inputWithDefaults);
1455
-		$task->setScheduledAt(time());
1456
-		$provider = $this->getPreferredProvider($task->getTaskTypeId());
1457
-		// calculate expected completion time
1458
-		$completionExpectedAt = new \DateTime('now');
1459
-		$completionExpectedAt->add(new \DateInterval('PT' . $provider->getExpectedRuntime() . 'S'));
1460
-		$task->setCompletionExpectedAt($completionExpectedAt);
1461
-	}
1462
-
1463
-	/**
1464
-	 * Store the task in the DB and set its ID in the \OCP\TaskProcessing\Task input param
1465
-	 *
1466
-	 * @param Task $task
1467
-	 * @return void
1468
-	 * @throws Exception
1469
-	 * @throws \JsonException
1470
-	 */
1471
-	private function storeTask(Task $task): void {
1472
-		// create a db entity and insert into db table
1473
-		$taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
1474
-		$this->taskMapper->insert($taskEntity);
1475
-		// make sure the scheduler knows the id
1476
-		$task->setId($taskEntity->getId());
1477
-	}
1478
-
1479
-	/**
1480
-	 * @param array $output
1481
-	 * @param ShapeDescriptor[] ...$specs the specs that define which keys to keep
1482
-	 * @return array
1483
-	 * @throws NotPermittedException
1484
-	 */
1485
-	private function validateOutputFileIds(array $output, ...$specs): array {
1486
-		$newOutput = [];
1487
-		$spec = array_reduce($specs, fn ($carry, $spec) => $carry + $spec, []);
1488
-		foreach ($spec as $key => $descriptor) {
1489
-			$type = $descriptor->getShapeType();
1490
-			if (!isset($output[$key])) {
1491
-				continue;
1492
-			}
1493
-			if (!in_array(EShapeType::getScalarType($type), [EShapeType::Image, EShapeType::Audio, EShapeType::Video, EShapeType::File], true)) {
1494
-				$newOutput[$key] = $output[$key];
1495
-				continue;
1496
-			}
1497
-			if (EShapeType::getScalarType($type) === $type) {
1498
-				// Is scalar file ID
1499
-				$newOutput[$key] = $this->validateFileId($output[$key]);
1500
-			} else {
1501
-				// Is list of file IDs
1502
-				$newOutput = [];
1503
-				foreach ($output[$key] as $item) {
1504
-					$newOutput[$key][] = $this->validateFileId($item);
1505
-				}
1506
-			}
1507
-		}
1508
-		return $newOutput;
1509
-	}
1510
-
1511
-	/**
1512
-	 * @param mixed $id
1513
-	 * @return File
1514
-	 * @throws ValidationException
1515
-	 */
1516
-	private function validateFileId(mixed $id): File {
1517
-		$node = $this->rootFolder->getFirstNodeById($id);
1518
-		if ($node === null) {
1519
-			$node = $this->rootFolder->getFirstNodeByIdInPath($id, '/' . $this->rootFolder->getAppDataDirectoryName() . '/');
1520
-			if ($node === null) {
1521
-				throw new ValidationException('Could not find file ' . $id);
1522
-			} elseif (!$node instanceof File) {
1523
-				throw new ValidationException('File with id "' . $id . '" is not a file');
1524
-			}
1525
-		} elseif (!$node instanceof File) {
1526
-			throw new ValidationException('File with id "' . $id . '" is not a file');
1527
-		}
1528
-		return $node;
1529
-	}
1530
-
1531
-	/**
1532
-	 * @param mixed $fileId
1533
-	 * @param string|null $userId
1534
-	 * @return void
1535
-	 * @throws UnauthorizedException
1536
-	 */
1537
-	private function validateUserAccessToFile(mixed $fileId, ?string $userId): void {
1538
-		if ($userId === null) {
1539
-			throw new UnauthorizedException('User does not have access to file ' . $fileId);
1540
-		}
1541
-		$mounts = $this->userMountCache->getMountsForFileId($fileId);
1542
-		$userIds = array_map(fn ($mount) => $mount->getUser()->getUID(), $mounts);
1543
-		if (!in_array($userId, $userIds)) {
1544
-			throw new UnauthorizedException('User ' . $userId . ' does not have access to file ' . $fileId);
1545
-		}
1546
-	}
1547
-
1548
-	/**
1549
-	 * @param Task $task
1550
-	 * @return list<int>
1551
-	 * @throws NotFoundException
1552
-	 */
1553
-	public function extractFileIdsFromTask(Task $task): array {
1554
-		$ids = [];
1555
-		$taskTypes = $this->getAvailableTaskTypes();
1556
-		if (!isset($taskTypes[$task->getTaskTypeId()])) {
1557
-			throw new NotFoundException('Could not find task type');
1558
-		}
1559
-		$taskType = $taskTypes[$task->getTaskTypeId()];
1560
-		foreach ($taskType['inputShape'] + $taskType['optionalInputShape'] as $key => $descriptor) {
1561
-			if (in_array(EShapeType::getScalarType($descriptor->getShapeType()), [EShapeType::File, EShapeType::Image, EShapeType::Audio, EShapeType::Video], true)) {
1562
-				/** @var int|list<int> $inputSlot */
1563
-				$inputSlot = $task->getInput()[$key];
1564
-				if (is_array($inputSlot)) {
1565
-					$ids = array_merge($inputSlot, $ids);
1566
-				} else {
1567
-					$ids[] = $inputSlot;
1568
-				}
1569
-			}
1570
-		}
1571
-		if ($task->getOutput() !== null) {
1572
-			foreach ($taskType['outputShape'] + $taskType['optionalOutputShape'] as $key => $descriptor) {
1573
-				if (in_array(EShapeType::getScalarType($descriptor->getShapeType()), [EShapeType::File, EShapeType::Image, EShapeType::Audio, EShapeType::Video], true)) {
1574
-					/** @var int|list<int> $outputSlot */
1575
-					$outputSlot = $task->getOutput()[$key];
1576
-					if (is_array($outputSlot)) {
1577
-						$ids = array_merge($outputSlot, $ids);
1578
-					} else {
1579
-						$ids[] = $outputSlot;
1580
-					}
1581
-				}
1582
-			}
1583
-		}
1584
-		return $ids;
1585
-	}
1586
-
1587
-	/**
1588
-	 * @param ISimpleFolder $folder
1589
-	 * @param int $ageInSeconds
1590
-	 * @return \Generator
1591
-	 */
1592
-	public function clearFilesOlderThan(ISimpleFolder $folder, int $ageInSeconds = self::MAX_TASK_AGE_SECONDS): \Generator {
1593
-		foreach ($folder->getDirectoryListing() as $file) {
1594
-			if ($file->getMTime() < time() - $ageInSeconds) {
1595
-				try {
1596
-					$fileName = $file->getName();
1597
-					$file->delete();
1598
-					yield $fileName;
1599
-				} catch (NotPermittedException $e) {
1600
-					$this->logger->warning('Failed to delete a stale task processing file', ['exception' => $e]);
1601
-				}
1602
-			}
1603
-		}
1604
-	}
1605
-
1606
-	/**
1607
-	 * @param int $ageInSeconds
1608
-	 * @return \Generator
1609
-	 * @throws Exception
1610
-	 * @throws InvalidPathException
1611
-	 * @throws NotFoundException
1612
-	 * @throws \JsonException
1613
-	 * @throws \OCP\Files\NotFoundException
1614
-	 */
1615
-	public function cleanupTaskProcessingTaskFiles(int $ageInSeconds = self::MAX_TASK_AGE_SECONDS): \Generator {
1616
-		$taskIdsToCleanup = [];
1617
-		foreach ($this->taskMapper->getTasksToCleanup($ageInSeconds) as $task) {
1618
-			$taskIdsToCleanup[] = $task->getId();
1619
-			$ocpTask = $task->toPublicTask();
1620
-			$fileIds = $this->extractFileIdsFromTask($ocpTask);
1621
-			foreach ($fileIds as $fileId) {
1622
-				// only look for output files stored in appData/TaskProcessing/
1623
-				$file = $this->rootFolder->getFirstNodeByIdInPath($fileId, '/' . $this->rootFolder->getAppDataDirectoryName() . '/core/TaskProcessing/');
1624
-				if ($file instanceof File) {
1625
-					try {
1626
-						$fileId = $file->getId();
1627
-						$fileName = $file->getName();
1628
-						$file->delete();
1629
-						yield ['task_id' => $task->getId(), 'file_id' => $fileId, 'file_name' => $fileName];
1630
-					} catch (NotPermittedException $e) {
1631
-						$this->logger->warning('Failed to delete a stale task processing file', ['exception' => $e]);
1632
-					}
1633
-				}
1634
-			}
1635
-		}
1636
-		return $taskIdsToCleanup;
1637
-	}
1638
-
1639
-	/**
1640
-	 * Make a request to the task's webhookUri if necessary
1641
-	 *
1642
-	 * @param Task $task
1643
-	 */
1644
-	private function runWebhook(Task $task): void {
1645
-		$uri = $task->getWebhookUri();
1646
-		$method = $task->getWebhookMethod();
1647
-
1648
-		if (!$uri || !$method) {
1649
-			return;
1650
-		}
1651
-
1652
-		if (in_array($method, ['HTTP:GET', 'HTTP:POST', 'HTTP:PUT', 'HTTP:DELETE'], true)) {
1653
-			$client = $this->clientService->newClient();
1654
-			$httpMethod = preg_replace('/^HTTP:/', '', $method);
1655
-			$options = [
1656
-				'timeout' => 30,
1657
-				'body' => json_encode([
1658
-					'task' => $task->jsonSerialize(),
1659
-				]),
1660
-				'headers' => ['Content-Type' => 'application/json'],
1661
-			];
1662
-			try {
1663
-				$client->request($httpMethod, $uri, $options);
1664
-			} catch (ClientException|ServerException $e) {
1665
-				$this->logger->warning('Task processing HTTP webhook failed for task ' . $task->getId() . '. Request failed', ['exception' => $e]);
1666
-			} catch (\Exception|\Throwable $e) {
1667
-				$this->logger->warning('Task processing HTTP webhook failed for task ' . $task->getId() . '. Unknown error', ['exception' => $e]);
1668
-			}
1669
-		} elseif (str_starts_with($method, 'AppAPI:') && str_starts_with($uri, '/')) {
1670
-			$parsedMethod = explode(':', $method, 4);
1671
-			if (count($parsedMethod) < 3) {
1672
-				$this->logger->warning('Task processing AppAPI webhook failed for task ' . $task->getId() . '. Invalid method: ' . $method);
1673
-			}
1674
-			[, $exAppId, $httpMethod] = $parsedMethod;
1675
-			if (!$this->appManager->isEnabledForAnyone('app_api')) {
1676
-				$this->logger->warning('Task processing AppAPI webhook failed for task ' . $task->getId() . '. AppAPI is disabled or not installed.');
1677
-				return;
1678
-			}
1679
-			try {
1680
-				$appApiFunctions = \OCP\Server::get(\OCA\AppAPI\PublicFunctions::class);
1681
-			} catch (ContainerExceptionInterface|NotFoundExceptionInterface) {
1682
-				$this->logger->warning('Task processing AppAPI webhook failed for task ' . $task->getId() . '. Could not get AppAPI public functions.');
1683
-				return;
1684
-			}
1685
-			$exApp = $appApiFunctions->getExApp($exAppId);
1686
-			if ($exApp === null) {
1687
-				$this->logger->warning('Task processing AppAPI webhook failed for task ' . $task->getId() . '. ExApp ' . $exAppId . ' is missing.');
1688
-				return;
1689
-			} elseif (!$exApp['enabled']) {
1690
-				$this->logger->warning('Task processing AppAPI webhook failed for task ' . $task->getId() . '. ExApp ' . $exAppId . ' is disabled.');
1691
-				return;
1692
-			}
1693
-			$requestParams = [
1694
-				'task' => $task->jsonSerialize(),
1695
-			];
1696
-			$requestOptions = [
1697
-				'timeout' => 30,
1698
-			];
1699
-			$response = $appApiFunctions->exAppRequest($exAppId, $uri, $task->getUserId(), $httpMethod, $requestParams, $requestOptions);
1700
-			if (is_array($response) && isset($response['error'])) {
1701
-				$this->logger->warning('Task processing AppAPI webhook failed for task ' . $task->getId() . '. Error during request to ExApp(' . $exAppId . '): ', $response['error']);
1702
-			}
1703
-		}
1704
-	}
76
+    public const LEGACY_PREFIX_TEXTPROCESSING = 'legacy:TextProcessing:';
77
+    public const LEGACY_PREFIX_TEXTTOIMAGE = 'legacy:TextToImage:';
78
+    public const LEGACY_PREFIX_SPEECHTOTEXT = 'legacy:SpeechToText:';
79
+
80
+    public const LAZY_CONFIG_KEYS = [
81
+        'ai.taskprocessing_type_preferences',
82
+        'ai.taskprocessing_provider_preferences',
83
+    ];
84
+
85
+    public const MAX_TASK_AGE_SECONDS = 60 * 60 * 24 * 31 * 6; // 6 months
86
+
87
+    private const TASK_TYPES_CACHE_KEY = 'available_task_types_v3';
88
+    private const TASK_TYPE_IDS_CACHE_KEY = 'available_task_type_ids';
89
+
90
+    /** @var list<IProvider>|null */
91
+    private ?array $providers = null;
92
+
93
+    /**
94
+     * @var array<array-key,array{name: string, description: string, inputShape: ShapeDescriptor[], inputShapeEnumValues: ShapeEnumValue[][], inputShapeDefaults: array<array-key, numeric|string>, isInternal: bool, optionalInputShape: ShapeDescriptor[], optionalInputShapeEnumValues: ShapeEnumValue[][], optionalInputShapeDefaults: array<array-key, numeric|string>, outputShape: ShapeDescriptor[], outputShapeEnumValues: ShapeEnumValue[][], optionalOutputShape: ShapeDescriptor[], optionalOutputShapeEnumValues: ShapeEnumValue[][]}>
95
+     */
96
+    private ?array $availableTaskTypes = null;
97
+
98
+    /** @var list<string>|null */
99
+    private ?array $availableTaskTypeIds = null;
100
+
101
+    private IAppData $appData;
102
+    private ?array $preferences = null;
103
+    private ?array $providersById = null;
104
+
105
+    /** @var ITaskType[]|null */
106
+    private ?array $taskTypes = null;
107
+    private ICache $distributedCache;
108
+
109
+    private ?GetTaskProcessingProvidersEvent $eventResult = null;
110
+
111
+    public function __construct(
112
+        private IAppConfig $appConfig,
113
+        private Coordinator $coordinator,
114
+        private IServerContainer $serverContainer,
115
+        private LoggerInterface $logger,
116
+        private TaskMapper $taskMapper,
117
+        private IJobList $jobList,
118
+        private IEventDispatcher $dispatcher,
119
+        IAppDataFactory $appDataFactory,
120
+        private IRootFolder $rootFolder,
121
+        private \OCP\TextToImage\IManager $textToImageManager,
122
+        private IUserMountCache $userMountCache,
123
+        private IClientService $clientService,
124
+        private IAppManager $appManager,
125
+        private IUserManager $userManager,
126
+        private IUserSession $userSession,
127
+        ICacheFactory $cacheFactory,
128
+        private IFactory $l10nFactory,
129
+    ) {
130
+        $this->appData = $appDataFactory->get('core');
131
+        $this->distributedCache = $cacheFactory->createDistributed('task_processing::');
132
+    }
133
+
134
+
135
+    /**
136
+     * This is almost a copy of textProcessingManager->getProviders
137
+     * to avoid a dependency cycle between TextProcessingManager and TaskProcessingManager
138
+     */
139
+    private function _getRawTextProcessingProviders(): array {
140
+        $context = $this->coordinator->getRegistrationContext();
141
+        if ($context === null) {
142
+            return [];
143
+        }
144
+
145
+        $providers = [];
146
+
147
+        foreach ($context->getTextProcessingProviders() as $providerServiceRegistration) {
148
+            $class = $providerServiceRegistration->getService();
149
+            try {
150
+                $providers[$class] = $this->serverContainer->get($class);
151
+            } catch (\Throwable $e) {
152
+                $this->logger->error('Failed to load Text processing provider ' . $class, [
153
+                    'exception' => $e,
154
+                ]);
155
+            }
156
+        }
157
+
158
+        return $providers;
159
+    }
160
+
161
+    private function _getTextProcessingProviders(): array {
162
+        $oldProviders = $this->_getRawTextProcessingProviders();
163
+        $newProviders = [];
164
+        foreach ($oldProviders as $oldProvider) {
165
+            $provider = new class($oldProvider) implements IProvider, ISynchronousProvider {
166
+                private \OCP\TextProcessing\IProvider $provider;
167
+
168
+                public function __construct(\OCP\TextProcessing\IProvider $provider) {
169
+                    $this->provider = $provider;
170
+                }
171
+
172
+                public function getId(): string {
173
+                    if ($this->provider instanceof \OCP\TextProcessing\IProviderWithId) {
174
+                        return $this->provider->getId();
175
+                    }
176
+                    return Manager::LEGACY_PREFIX_TEXTPROCESSING . $this->provider::class;
177
+                }
178
+
179
+                public function getName(): string {
180
+                    return $this->provider->getName();
181
+                }
182
+
183
+                public function getTaskTypeId(): string {
184
+                    return match ($this->provider->getTaskType()) {
185
+                        \OCP\TextProcessing\FreePromptTaskType::class => TextToText::ID,
186
+                        \OCP\TextProcessing\HeadlineTaskType::class => TextToTextHeadline::ID,
187
+                        \OCP\TextProcessing\TopicsTaskType::class => TextToTextTopics::ID,
188
+                        \OCP\TextProcessing\SummaryTaskType::class => TextToTextSummary::ID,
189
+                        default => Manager::LEGACY_PREFIX_TEXTPROCESSING . $this->provider->getTaskType(),
190
+                    };
191
+                }
192
+
193
+                public function getExpectedRuntime(): int {
194
+                    if ($this->provider instanceof \OCP\TextProcessing\IProviderWithExpectedRuntime) {
195
+                        return $this->provider->getExpectedRuntime();
196
+                    }
197
+                    return 60;
198
+                }
199
+
200
+                public function getOptionalInputShape(): array {
201
+                    return [];
202
+                }
203
+
204
+                public function getOptionalOutputShape(): array {
205
+                    return [];
206
+                }
207
+
208
+                public function process(?string $userId, array $input, callable $reportProgress): array {
209
+                    if ($this->provider instanceof \OCP\TextProcessing\IProviderWithUserId) {
210
+                        $this->provider->setUserId($userId);
211
+                    }
212
+                    try {
213
+                        return ['output' => $this->provider->process($input['input'])];
214
+                    } catch (\RuntimeException $e) {
215
+                        throw new ProcessingException($e->getMessage(), previous: $e);
216
+                    }
217
+                }
218
+
219
+                public function getInputShapeEnumValues(): array {
220
+                    return [];
221
+                }
222
+
223
+                public function getInputShapeDefaults(): array {
224
+                    return [];
225
+                }
226
+
227
+                public function getOptionalInputShapeEnumValues(): array {
228
+                    return [];
229
+                }
230
+
231
+                public function getOptionalInputShapeDefaults(): array {
232
+                    return [];
233
+                }
234
+
235
+                public function getOutputShapeEnumValues(): array {
236
+                    return [];
237
+                }
238
+
239
+                public function getOptionalOutputShapeEnumValues(): array {
240
+                    return [];
241
+                }
242
+            };
243
+            $newProviders[$provider->getId()] = $provider;
244
+        }
245
+
246
+        return $newProviders;
247
+    }
248
+
249
+    /**
250
+     * @return ITaskType[]
251
+     */
252
+    private function _getTextProcessingTaskTypes(): array {
253
+        $oldProviders = $this->_getRawTextProcessingProviders();
254
+        $newTaskTypes = [];
255
+        foreach ($oldProviders as $oldProvider) {
256
+            // These are already implemented in the TaskProcessing realm
257
+            if (in_array($oldProvider->getTaskType(), [
258
+                \OCP\TextProcessing\FreePromptTaskType::class,
259
+                \OCP\TextProcessing\HeadlineTaskType::class,
260
+                \OCP\TextProcessing\TopicsTaskType::class,
261
+                \OCP\TextProcessing\SummaryTaskType::class
262
+            ], true)) {
263
+                continue;
264
+            }
265
+            $taskType = new class($oldProvider->getTaskType()) implements ITaskType {
266
+                private string $oldTaskTypeClass;
267
+                private \OCP\TextProcessing\ITaskType $oldTaskType;
268
+                private IL10N $l;
269
+
270
+                public function __construct(string $oldTaskTypeClass) {
271
+                    $this->oldTaskTypeClass = $oldTaskTypeClass;
272
+                    $this->oldTaskType = \OCP\Server::get($oldTaskTypeClass);
273
+                    $this->l = \OCP\Server::get(IFactory::class)->get('core');
274
+                }
275
+
276
+                public function getId(): string {
277
+                    return Manager::LEGACY_PREFIX_TEXTPROCESSING . $this->oldTaskTypeClass;
278
+                }
279
+
280
+                public function getName(): string {
281
+                    return $this->oldTaskType->getName();
282
+                }
283
+
284
+                public function getDescription(): string {
285
+                    return $this->oldTaskType->getDescription();
286
+                }
287
+
288
+                public function getInputShape(): array {
289
+                    return ['input' => new ShapeDescriptor($this->l->t('Input text'), $this->l->t('The input text'), EShapeType::Text)];
290
+                }
291
+
292
+                public function getOutputShape(): array {
293
+                    return ['output' => new ShapeDescriptor($this->l->t('Input text'), $this->l->t('The input text'), EShapeType::Text)];
294
+                }
295
+            };
296
+            $newTaskTypes[$taskType->getId()] = $taskType;
297
+        }
298
+
299
+        return $newTaskTypes;
300
+    }
301
+
302
+    /**
303
+     * @return IProvider[]
304
+     */
305
+    private function _getTextToImageProviders(): array {
306
+        $oldProviders = $this->textToImageManager->getProviders();
307
+        $newProviders = [];
308
+        foreach ($oldProviders as $oldProvider) {
309
+            $newProvider = new class($oldProvider, $this->appData) implements IProvider, ISynchronousProvider {
310
+                private \OCP\TextToImage\IProvider $provider;
311
+                private IAppData $appData;
312
+
313
+                public function __construct(\OCP\TextToImage\IProvider $provider, IAppData $appData) {
314
+                    $this->provider = $provider;
315
+                    $this->appData = $appData;
316
+                }
317
+
318
+                public function getId(): string {
319
+                    return Manager::LEGACY_PREFIX_TEXTTOIMAGE . $this->provider->getId();
320
+                }
321
+
322
+                public function getName(): string {
323
+                    return $this->provider->getName();
324
+                }
325
+
326
+                public function getTaskTypeId(): string {
327
+                    return TextToImage::ID;
328
+                }
329
+
330
+                public function getExpectedRuntime(): int {
331
+                    return $this->provider->getExpectedRuntime();
332
+                }
333
+
334
+                public function getOptionalInputShape(): array {
335
+                    return [];
336
+                }
337
+
338
+                public function getOptionalOutputShape(): array {
339
+                    return [];
340
+                }
341
+
342
+                public function process(?string $userId, array $input, callable $reportProgress): array {
343
+                    try {
344
+                        $folder = $this->appData->getFolder('text2image');
345
+                    } catch (\OCP\Files\NotFoundException) {
346
+                        $folder = $this->appData->newFolder('text2image');
347
+                    }
348
+                    $resources = [];
349
+                    $files = [];
350
+                    for ($i = 0; $i < $input['numberOfImages']; $i++) {
351
+                        $file = $folder->newFile(time() . '-' . rand(1, 100000) . '-' . $i);
352
+                        $files[] = $file;
353
+                        $resource = $file->write();
354
+                        if ($resource !== false && $resource !== true && is_resource($resource)) {
355
+                            $resources[] = $resource;
356
+                        } else {
357
+                            throw new ProcessingException('Text2Image generation using provider "' . $this->getName() . '" failed: Couldn\'t open file to write.');
358
+                        }
359
+                    }
360
+                    if ($this->provider instanceof \OCP\TextToImage\IProviderWithUserId) {
361
+                        $this->provider->setUserId($userId);
362
+                    }
363
+                    try {
364
+                        $this->provider->generate($input['input'], $resources);
365
+                    } catch (\RuntimeException $e) {
366
+                        throw new ProcessingException($e->getMessage(), previous: $e);
367
+                    }
368
+                    for ($i = 0; $i < $input['numberOfImages']; $i++) {
369
+                        if (is_resource($resources[$i])) {
370
+                            // If $resource hasn't been closed yet, we'll do that here
371
+                            fclose($resources[$i]);
372
+                        }
373
+                    }
374
+                    return ['images' => array_map(fn (ISimpleFile $file) => $file->getContent(), $files)];
375
+                }
376
+
377
+                public function getInputShapeEnumValues(): array {
378
+                    return [];
379
+                }
380
+
381
+                public function getInputShapeDefaults(): array {
382
+                    return [];
383
+                }
384
+
385
+                public function getOptionalInputShapeEnumValues(): array {
386
+                    return [];
387
+                }
388
+
389
+                public function getOptionalInputShapeDefaults(): array {
390
+                    return [];
391
+                }
392
+
393
+                public function getOutputShapeEnumValues(): array {
394
+                    return [];
395
+                }
396
+
397
+                public function getOptionalOutputShapeEnumValues(): array {
398
+                    return [];
399
+                }
400
+            };
401
+            $newProviders[$newProvider->getId()] = $newProvider;
402
+        }
403
+
404
+        return $newProviders;
405
+    }
406
+
407
+    /**
408
+     * This is almost a copy of SpeechToTextManager->getProviders
409
+     * to avoid a dependency cycle between SpeechToTextManager and TaskProcessingManager
410
+     */
411
+    private function _getRawSpeechToTextProviders(): array {
412
+        $context = $this->coordinator->getRegistrationContext();
413
+        if ($context === null) {
414
+            return [];
415
+        }
416
+        $providers = [];
417
+        foreach ($context->getSpeechToTextProviders() as $providerServiceRegistration) {
418
+            $class = $providerServiceRegistration->getService();
419
+            try {
420
+                $providers[$class] = $this->serverContainer->get($class);
421
+            } catch (NotFoundExceptionInterface|ContainerExceptionInterface|\Throwable $e) {
422
+                $this->logger->error('Failed to load SpeechToText provider ' . $class, [
423
+                    'exception' => $e,
424
+                ]);
425
+            }
426
+        }
427
+
428
+        return $providers;
429
+    }
430
+
431
+    /**
432
+     * @return IProvider[]
433
+     */
434
+    private function _getSpeechToTextProviders(): array {
435
+        $oldProviders = $this->_getRawSpeechToTextProviders();
436
+        $newProviders = [];
437
+        foreach ($oldProviders as $oldProvider) {
438
+            $newProvider = new class($oldProvider, $this->rootFolder, $this->appData) implements IProvider, ISynchronousProvider {
439
+                private ISpeechToTextProvider $provider;
440
+                private IAppData $appData;
441
+
442
+                private IRootFolder $rootFolder;
443
+
444
+                public function __construct(ISpeechToTextProvider $provider, IRootFolder $rootFolder, IAppData $appData) {
445
+                    $this->provider = $provider;
446
+                    $this->rootFolder = $rootFolder;
447
+                    $this->appData = $appData;
448
+                }
449
+
450
+                public function getId(): string {
451
+                    if ($this->provider instanceof ISpeechToTextProviderWithId) {
452
+                        return Manager::LEGACY_PREFIX_SPEECHTOTEXT . $this->provider->getId();
453
+                    }
454
+                    return Manager::LEGACY_PREFIX_SPEECHTOTEXT . $this->provider::class;
455
+                }
456
+
457
+                public function getName(): string {
458
+                    return $this->provider->getName();
459
+                }
460
+
461
+                public function getTaskTypeId(): string {
462
+                    return AudioToText::ID;
463
+                }
464
+
465
+                public function getExpectedRuntime(): int {
466
+                    return 60;
467
+                }
468
+
469
+                public function getOptionalInputShape(): array {
470
+                    return [];
471
+                }
472
+
473
+                public function getOptionalOutputShape(): array {
474
+                    return [];
475
+                }
476
+
477
+                public function process(?string $userId, array $input, callable $reportProgress): array {
478
+                    if ($this->provider instanceof \OCP\SpeechToText\ISpeechToTextProviderWithUserId) {
479
+                        $this->provider->setUserId($userId);
480
+                    }
481
+                    try {
482
+                        $result = $this->provider->transcribeFile($input['input']);
483
+                    } catch (\RuntimeException $e) {
484
+                        throw new ProcessingException($e->getMessage(), previous: $e);
485
+                    }
486
+                    return ['output' => $result];
487
+                }
488
+
489
+                public function getInputShapeEnumValues(): array {
490
+                    return [];
491
+                }
492
+
493
+                public function getInputShapeDefaults(): array {
494
+                    return [];
495
+                }
496
+
497
+                public function getOptionalInputShapeEnumValues(): array {
498
+                    return [];
499
+                }
500
+
501
+                public function getOptionalInputShapeDefaults(): array {
502
+                    return [];
503
+                }
504
+
505
+                public function getOutputShapeEnumValues(): array {
506
+                    return [];
507
+                }
508
+
509
+                public function getOptionalOutputShapeEnumValues(): array {
510
+                    return [];
511
+                }
512
+            };
513
+            $newProviders[$newProvider->getId()] = $newProvider;
514
+        }
515
+
516
+        return $newProviders;
517
+    }
518
+
519
+    /**
520
+     * Dispatches the event to collect external providers and task types.
521
+     * Caches the result within the request.
522
+     */
523
+    private function dispatchGetProvidersEvent(): GetTaskProcessingProvidersEvent {
524
+        if ($this->eventResult !== null) {
525
+            return $this->eventResult;
526
+        }
527
+
528
+        $this->eventResult = new GetTaskProcessingProvidersEvent();
529
+        $this->dispatcher->dispatchTyped($this->eventResult);
530
+        return $this->eventResult ;
531
+    }
532
+
533
+    /**
534
+     * @return IProvider[]
535
+     */
536
+    private function _getProviders(): array {
537
+        $context = $this->coordinator->getRegistrationContext();
538
+
539
+        if ($context === null) {
540
+            return [];
541
+        }
542
+
543
+        $providers = [];
544
+
545
+        foreach ($context->getTaskProcessingProviders() as $providerServiceRegistration) {
546
+            $class = $providerServiceRegistration->getService();
547
+            try {
548
+                /** @var IProvider $provider */
549
+                $provider = $this->serverContainer->get($class);
550
+                if (isset($providers[$provider->getId()])) {
551
+                    $this->logger->warning('Task processing provider ' . $class . ' is using ID ' . $provider->getId() . ' which is already used by ' . $providers[$provider->getId()]::class);
552
+                }
553
+                $providers[$provider->getId()] = $provider;
554
+            } catch (\Throwable $e) {
555
+                $this->logger->error('Failed to load task processing provider ' . $class, [
556
+                    'exception' => $e,
557
+                ]);
558
+            }
559
+        }
560
+
561
+        $event = $this->dispatchGetProvidersEvent();
562
+        $externalProviders = $event->getProviders();
563
+        foreach ($externalProviders as $provider) {
564
+            if (!isset($providers[$provider->getId()])) {
565
+                $providers[$provider->getId()] = $provider;
566
+            } else {
567
+                $this->logger->info('Skipping external task processing provider with ID ' . $provider->getId() . ' because a local provider with the same ID already exists.');
568
+            }
569
+        }
570
+
571
+        $providers += $this->_getTextProcessingProviders() + $this->_getTextToImageProviders() + $this->_getSpeechToTextProviders();
572
+
573
+        return $providers;
574
+    }
575
+
576
+    /**
577
+     * @return ITaskType[]
578
+     */
579
+    private function _getTaskTypes(): array {
580
+        $context = $this->coordinator->getRegistrationContext();
581
+
582
+        if ($context === null) {
583
+            return [];
584
+        }
585
+
586
+        if ($this->taskTypes !== null) {
587
+            return $this->taskTypes;
588
+        }
589
+
590
+        // Default task types
591
+        $taskTypes = [
592
+            \OCP\TaskProcessing\TaskTypes\TextToText::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToText::class),
593
+            \OCP\TaskProcessing\TaskTypes\TextToTextTopics::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextTopics::class),
594
+            \OCP\TaskProcessing\TaskTypes\TextToTextHeadline::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextHeadline::class),
595
+            \OCP\TaskProcessing\TaskTypes\TextToTextSummary::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextSummary::class),
596
+            \OCP\TaskProcessing\TaskTypes\TextToTextFormalization::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextFormalization::class),
597
+            \OCP\TaskProcessing\TaskTypes\TextToTextSimplification::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextSimplification::class),
598
+            \OCP\TaskProcessing\TaskTypes\TextToTextChat::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextChat::class),
599
+            \OCP\TaskProcessing\TaskTypes\TextToTextTranslate::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextTranslate::class),
600
+            \OCP\TaskProcessing\TaskTypes\TextToTextReformulation::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextReformulation::class),
601
+            \OCP\TaskProcessing\TaskTypes\TextToImage::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToImage::class),
602
+            \OCP\TaskProcessing\TaskTypes\AudioToText::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\AudioToText::class),
603
+            \OCP\TaskProcessing\TaskTypes\ContextWrite::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\ContextWrite::class),
604
+            \OCP\TaskProcessing\TaskTypes\GenerateEmoji::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\GenerateEmoji::class),
605
+            \OCP\TaskProcessing\TaskTypes\TextToTextChangeTone::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextChangeTone::class),
606
+            \OCP\TaskProcessing\TaskTypes\TextToTextChatWithTools::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextChatWithTools::class),
607
+            \OCP\TaskProcessing\TaskTypes\ContextAgentInteraction::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\ContextAgentInteraction::class),
608
+            \OCP\TaskProcessing\TaskTypes\TextToTextProofread::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToTextProofread::class),
609
+            \OCP\TaskProcessing\TaskTypes\TextToSpeech::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\TextToSpeech::class),
610
+            \OCP\TaskProcessing\TaskTypes\AudioToAudioChat::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\AudioToAudioChat::class),
611
+            \OCP\TaskProcessing\TaskTypes\ContextAgentAudioInteraction::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\ContextAgentAudioInteraction::class),
612
+            \OCP\TaskProcessing\TaskTypes\AnalyzeImages::ID => \OCP\Server::get(\OCP\TaskProcessing\TaskTypes\AnalyzeImages::class),
613
+        ];
614
+
615
+        foreach ($context->getTaskProcessingTaskTypes() as $providerServiceRegistration) {
616
+            $class = $providerServiceRegistration->getService();
617
+            try {
618
+                /** @var ITaskType $provider */
619
+                $taskType = $this->serverContainer->get($class);
620
+                if (isset($taskTypes[$taskType->getId()])) {
621
+                    $this->logger->warning('Task processing task type ' . $class . ' is using ID ' . $taskType->getId() . ' which is already used by ' . $taskTypes[$taskType->getId()]::class);
622
+                }
623
+                $taskTypes[$taskType->getId()] = $taskType;
624
+            } catch (\Throwable $e) {
625
+                $this->logger->error('Failed to load task processing task type ' . $class, [
626
+                    'exception' => $e,
627
+                ]);
628
+            }
629
+        }
630
+
631
+        $event = $this->dispatchGetProvidersEvent();
632
+        $externalTaskTypes = $event->getTaskTypes();
633
+        foreach ($externalTaskTypes as $taskType) {
634
+            if (isset($taskTypes[$taskType->getId()])) {
635
+                $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()]) . ')');
636
+            }
637
+            $taskTypes[$taskType->getId()] = $taskType;
638
+        }
639
+
640
+        $taskTypes += $this->_getTextProcessingTaskTypes();
641
+
642
+        $this->taskTypes = $taskTypes;
643
+        return $this->taskTypes;
644
+    }
645
+
646
+    /**
647
+     * @return array
648
+     */
649
+    private function _getTaskTypeSettings(): array {
650
+        try {
651
+            $json = $this->appConfig->getValueString('core', 'ai.taskprocessing_type_preferences', '', lazy: true);
652
+            if ($json === '') {
653
+                return [];
654
+            }
655
+            return json_decode($json, true, flags: JSON_THROW_ON_ERROR);
656
+        } catch (\JsonException $e) {
657
+            $this->logger->error('Failed to get settings. JSON Error in ai.taskprocessing_type_preferences', ['exception' => $e]);
658
+            $taskTypeSettings = [];
659
+            $taskTypes = $this->_getTaskTypes();
660
+            foreach ($taskTypes as $taskType) {
661
+                $taskTypeSettings[$taskType->getId()] = false;
662
+            };
663
+
664
+            return $taskTypeSettings;
665
+        }
666
+
667
+    }
668
+
669
+    /**
670
+     * @param ShapeDescriptor[] $spec
671
+     * @param array<array-key, string|numeric> $defaults
672
+     * @param array<array-key, ShapeEnumValue[]> $enumValues
673
+     * @param array $io
674
+     * @param bool $optional
675
+     * @return void
676
+     * @throws ValidationException
677
+     */
678
+    private static function validateInput(array $spec, array $defaults, array $enumValues, array $io, bool $optional = false): void {
679
+        foreach ($spec as $key => $descriptor) {
680
+            $type = $descriptor->getShapeType();
681
+            if (!isset($io[$key])) {
682
+                if ($optional) {
683
+                    continue;
684
+                }
685
+                if (isset($defaults[$key])) {
686
+                    if (EShapeType::getScalarType($type) !== $type) {
687
+                        throw new ValidationException('Provider tried to set a default value for a non-scalar slot');
688
+                    }
689
+                    if (EShapeType::isFileType($type)) {
690
+                        throw new ValidationException('Provider tried to set a default value for a slot that is not text or number');
691
+                    }
692
+                    $type->validateInput($defaults[$key]);
693
+                    continue;
694
+                }
695
+                throw new ValidationException('Missing key: "' . $key . '"');
696
+            }
697
+            try {
698
+                $type->validateInput($io[$key]);
699
+                if ($type === EShapeType::Enum) {
700
+                    if (!isset($enumValues[$key])) {
701
+                        throw new ValidationException('Provider did not provide enum values for an enum slot: "' . $key . '"');
702
+                    }
703
+                    $type->validateEnum($io[$key], $enumValues[$key]);
704
+                }
705
+            } catch (ValidationException $e) {
706
+                throw new ValidationException('Failed to validate input key "' . $key . '": ' . $e->getMessage());
707
+            }
708
+        }
709
+    }
710
+
711
+    /**
712
+     * Takes task input data and replaces fileIds with File objects
713
+     *
714
+     * @param array<array-key, list<numeric|string>|numeric|string> $input
715
+     * @param array<array-key, numeric|string> ...$defaultSpecs the specs
716
+     * @return array<array-key, list<numeric|string>|numeric|string>
717
+     */
718
+    public function fillInputDefaults(array $input, ...$defaultSpecs): array {
719
+        $spec = array_reduce($defaultSpecs, fn ($carry, $spec) => array_merge($carry, $spec), []);
720
+        return array_merge($spec, $input);
721
+    }
722
+
723
+    /**
724
+     * @param ShapeDescriptor[] $spec
725
+     * @param array<array-key, ShapeEnumValue[]> $enumValues
726
+     * @param array $io
727
+     * @param bool $optional
728
+     * @return void
729
+     * @throws ValidationException
730
+     */
731
+    private static function validateOutputWithFileIds(array $spec, array $enumValues, array $io, bool $optional = false): void {
732
+        foreach ($spec as $key => $descriptor) {
733
+            $type = $descriptor->getShapeType();
734
+            if (!isset($io[$key])) {
735
+                if ($optional) {
736
+                    continue;
737
+                }
738
+                throw new ValidationException('Missing key: "' . $key . '"');
739
+            }
740
+            try {
741
+                $type->validateOutputWithFileIds($io[$key]);
742
+                if (isset($enumValues[$key])) {
743
+                    $type->validateEnum($io[$key], $enumValues[$key]);
744
+                }
745
+            } catch (ValidationException $e) {
746
+                throw new ValidationException('Failed to validate output key "' . $key . '": ' . $e->getMessage());
747
+            }
748
+        }
749
+    }
750
+
751
+    /**
752
+     * @param ShapeDescriptor[] $spec
753
+     * @param array<array-key, ShapeEnumValue[]> $enumValues
754
+     * @param array $io
755
+     * @param bool $optional
756
+     * @return void
757
+     * @throws ValidationException
758
+     */
759
+    private static function validateOutputWithFileData(array $spec, array $enumValues, array $io, bool $optional = false): void {
760
+        foreach ($spec as $key => $descriptor) {
761
+            $type = $descriptor->getShapeType();
762
+            if (!isset($io[$key])) {
763
+                if ($optional) {
764
+                    continue;
765
+                }
766
+                throw new ValidationException('Missing key: "' . $key . '"');
767
+            }
768
+            try {
769
+                $type->validateOutputWithFileData($io[$key]);
770
+                if (isset($enumValues[$key])) {
771
+                    $type->validateEnum($io[$key], $enumValues[$key]);
772
+                }
773
+            } catch (ValidationException $e) {
774
+                throw new ValidationException('Failed to validate output key "' . $key . '": ' . $e->getMessage());
775
+            }
776
+        }
777
+    }
778
+
779
+    /**
780
+     * @param array<array-key, T> $array The array to filter
781
+     * @param ShapeDescriptor[] ...$specs the specs that define which keys to keep
782
+     * @return array<array-key, T>
783
+     * @psalm-template T
784
+     */
785
+    private function removeSuperfluousArrayKeys(array $array, ...$specs): array {
786
+        $keys = array_unique(array_reduce($specs, fn ($carry, $spec) => array_merge($carry, array_keys($spec)), []));
787
+        $keys = array_filter($keys, fn ($key) => array_key_exists($key, $array));
788
+        $values = array_map(fn (string $key) => $array[$key], $keys);
789
+        return array_combine($keys, $values);
790
+    }
791
+
792
+    public function hasProviders(): bool {
793
+        return count($this->getProviders()) !== 0;
794
+    }
795
+
796
+    public function getProviders(): array {
797
+        if ($this->providers === null) {
798
+            $this->providers = $this->_getProviders();
799
+        }
800
+
801
+        return $this->providers;
802
+    }
803
+
804
+    public function getPreferredProvider(string $taskTypeId) {
805
+        try {
806
+            if ($this->preferences === null) {
807
+                $this->preferences = $this->distributedCache->get('ai.taskprocessing_provider_preferences');
808
+                if ($this->preferences === null) {
809
+                    $this->preferences = json_decode(
810
+                        $this->appConfig->getValueString('core', 'ai.taskprocessing_provider_preferences', 'null', lazy: true),
811
+                        associative: true,
812
+                        flags: JSON_THROW_ON_ERROR,
813
+                    );
814
+                    $this->distributedCache->set('ai.taskprocessing_provider_preferences', $this->preferences, 60 * 3);
815
+                }
816
+            }
817
+
818
+            $providers = $this->getProviders();
819
+            if (isset($this->preferences[$taskTypeId])) {
820
+                $providersById = $this->providersById ?? array_reduce($providers, static function (array $carry, IProvider $provider) {
821
+                    $carry[$provider->getId()] = $provider;
822
+                    return $carry;
823
+                }, []);
824
+                $this->providersById = $providersById;
825
+                if (isset($providersById[$this->preferences[$taskTypeId]])) {
826
+                    return $providersById[$this->preferences[$taskTypeId]];
827
+                }
828
+            }
829
+            // By default, use the first available provider
830
+            foreach ($providers as $provider) {
831
+                if ($provider->getTaskTypeId() === $taskTypeId) {
832
+                    return $provider;
833
+                }
834
+            }
835
+        } catch (\JsonException $e) {
836
+            $this->logger->warning('Failed to parse provider preferences while getting preferred provider for task type ' . $taskTypeId, ['exception' => $e]);
837
+        }
838
+        throw new \OCP\TaskProcessing\Exception\Exception('No matching provider found');
839
+    }
840
+
841
+    public function getAvailableTaskTypes(bool $showDisabled = false, ?string $userId = null): array {
842
+        // We cache by language, because some task type fields are translated
843
+        $cacheKey = self::TASK_TYPES_CACHE_KEY . ':' . $this->l10nFactory->findLanguage();
844
+
845
+        // userId will be obtained from the session if left to null
846
+        if (!$this->checkGuestAccess($userId)) {
847
+            return [];
848
+        }
849
+        if ($this->availableTaskTypes === null) {
850
+            $cachedValue = $this->distributedCache->get($cacheKey);
851
+            if ($cachedValue !== null) {
852
+                $this->availableTaskTypes = unserialize($cachedValue);
853
+            }
854
+        }
855
+        // Either we have no cache or showDisabled is turned on, which we don't want to cache, ever.
856
+        if ($this->availableTaskTypes === null || $showDisabled) {
857
+            $taskTypes = $this->_getTaskTypes();
858
+            $taskTypeSettings = $this->_getTaskTypeSettings();
859
+
860
+            $availableTaskTypes = [];
861
+            foreach ($taskTypes as $taskType) {
862
+                if ((!$showDisabled) && isset($taskTypeSettings[$taskType->getId()]) && !$taskTypeSettings[$taskType->getId()]) {
863
+                    continue;
864
+                }
865
+                try {
866
+                    $provider = $this->getPreferredProvider($taskType->getId());
867
+                } catch (\OCP\TaskProcessing\Exception\Exception $e) {
868
+                    continue;
869
+                }
870
+                try {
871
+                    $availableTaskTypes[$provider->getTaskTypeId()] = [
872
+                        'name' => $taskType->getName(),
873
+                        'description' => $taskType->getDescription(),
874
+                        'optionalInputShape' => $provider->getOptionalInputShape(),
875
+                        'inputShapeEnumValues' => $provider->getInputShapeEnumValues(),
876
+                        'inputShapeDefaults' => $provider->getInputShapeDefaults(),
877
+                        'inputShape' => $taskType->getInputShape(),
878
+                        'optionalInputShapeEnumValues' => $provider->getOptionalInputShapeEnumValues(),
879
+                        'optionalInputShapeDefaults' => $provider->getOptionalInputShapeDefaults(),
880
+                        'outputShape' => $taskType->getOutputShape(),
881
+                        'outputShapeEnumValues' => $provider->getOutputShapeEnumValues(),
882
+                        'optionalOutputShape' => $provider->getOptionalOutputShape(),
883
+                        'optionalOutputShapeEnumValues' => $provider->getOptionalOutputShapeEnumValues(),
884
+                        'isInternal' => $taskType instanceof IInternalTaskType,
885
+                    ];
886
+                } catch (\Throwable $e) {
887
+                    $this->logger->error('Failed to set up TaskProcessing provider ' . $provider::class, ['exception' => $e]);
888
+                }
889
+            }
890
+
891
+            if ($showDisabled) {
892
+                // Do not cache showDisabled, ever.
893
+                return $availableTaskTypes;
894
+            }
895
+
896
+            $this->availableTaskTypes = $availableTaskTypes;
897
+            $this->distributedCache->set($cacheKey, serialize($this->availableTaskTypes), 60);
898
+        }
899
+
900
+
901
+        return $this->availableTaskTypes;
902
+    }
903
+    public function getAvailableTaskTypeIds(bool $showDisabled = false, ?string $userId = null): array {
904
+        // userId will be obtained from the session if left to null
905
+        if (!$this->checkGuestAccess($userId)) {
906
+            return [];
907
+        }
908
+        if ($this->availableTaskTypeIds === null) {
909
+            $cachedValue = $this->distributedCache->get(self::TASK_TYPE_IDS_CACHE_KEY);
910
+            if ($cachedValue !== null) {
911
+                $this->availableTaskTypeIds = $cachedValue;
912
+            }
913
+        }
914
+        // Either we have no cache or showDisabled is turned on, which we don't want to cache, ever.
915
+        if ($this->availableTaskTypeIds === null || $showDisabled) {
916
+            $taskTypes = $this->_getTaskTypes();
917
+            $taskTypeSettings = $this->_getTaskTypeSettings();
918
+
919
+            $availableTaskTypeIds = [];
920
+            foreach ($taskTypes as $taskType) {
921
+                if ((!$showDisabled) && isset($taskTypeSettings[$taskType->getId()]) && !$taskTypeSettings[$taskType->getId()]) {
922
+                    continue;
923
+                }
924
+                try {
925
+                    $provider = $this->getPreferredProvider($taskType->getId());
926
+                } catch (\OCP\TaskProcessing\Exception\Exception $e) {
927
+                    continue;
928
+                }
929
+                $availableTaskTypeIds[] = $taskType->getId();
930
+            }
931
+
932
+            if ($showDisabled) {
933
+                // Do not cache showDisabled, ever.
934
+                return $availableTaskTypeIds;
935
+            }
936
+
937
+            $this->availableTaskTypeIds = $availableTaskTypeIds;
938
+            $this->distributedCache->set(self::TASK_TYPE_IDS_CACHE_KEY, $this->availableTaskTypeIds, 60);
939
+        }
940
+
941
+
942
+        return $this->availableTaskTypeIds;
943
+    }
944
+
945
+    public function canHandleTask(Task $task): bool {
946
+        return isset($this->getAvailableTaskTypes()[$task->getTaskTypeId()]);
947
+    }
948
+
949
+    private function checkGuestAccess(?string $userId = null): bool {
950
+        if ($userId === null && !$this->userSession->isLoggedIn()) {
951
+            return true;
952
+        }
953
+        if ($userId === null) {
954
+            $user = $this->userSession->getUser();
955
+        } else {
956
+            $user = $this->userManager->get($userId);
957
+        }
958
+
959
+        $guestsAllowed = $this->appConfig->getValueString('core', 'ai.taskprocessing_guests', 'false');
960
+        if ($guestsAllowed == 'true' || !class_exists(\OCA\Guests\UserBackend::class) || !($user->getBackend() instanceof \OCA\Guests\UserBackend)) {
961
+            return true;
962
+        }
963
+        return false;
964
+    }
965
+
966
+    public function scheduleTask(Task $task): void {
967
+        if (!$this->checkGuestAccess($task->getUserId())) {
968
+            throw new \OCP\TaskProcessing\Exception\PreConditionNotMetException('Access to this resource is forbidden for guests.');
969
+        }
970
+        if (!$this->canHandleTask($task)) {
971
+            throw new \OCP\TaskProcessing\Exception\PreConditionNotMetException('No task processing provider is installed that can handle this task type: ' . $task->getTaskTypeId());
972
+        }
973
+        $this->prepareTask($task);
974
+        $task->setStatus(Task::STATUS_SCHEDULED);
975
+        $this->storeTask($task);
976
+        // schedule synchronous job if the provider is synchronous
977
+        $provider = $this->getPreferredProvider($task->getTaskTypeId());
978
+        if ($provider instanceof ISynchronousProvider) {
979
+            $this->jobList->add(SynchronousBackgroundJob::class, null);
980
+        }
981
+        if ($provider instanceof ITriggerableProvider) {
982
+            try {
983
+                if (!$this->taskMapper->hasRunningTasksForTaskType($task->getTaskTypeId())) {
984
+                    // If no tasks are currently running for this task type, nudge the provider to ask for tasks
985
+                    try {
986
+                        $provider->trigger();
987
+                    } catch (\Throwable $e) {
988
+                        $this->logger->error('Failed to trigger the provider after scheduling a task.', [
989
+                            'exception' => $e,
990
+                            'taskId' => $task->getId(),
991
+                            'providerId' => $provider->getId(),
992
+                        ]);
993
+                    }
994
+                }
995
+            } catch (Exception $e) {
996
+                $this->logger->error('Failed to check DB for running tasks after a task was scheduled for a triggerable provider. Not triggering the provider.', [
997
+                    'exception' => $e,
998
+                    'taskId' => $task->getId(),
999
+                    'providerId' => $provider->getId()
1000
+                ]);
1001
+            }
1002
+        }
1003
+    }
1004
+
1005
+    public function runTask(Task $task): Task {
1006
+        if (!$this->checkGuestAccess($task->getUserId())) {
1007
+            throw new \OCP\TaskProcessing\Exception\PreConditionNotMetException('Access to this resource is forbidden for guests.');
1008
+        }
1009
+        if (!$this->canHandleTask($task)) {
1010
+            throw new \OCP\TaskProcessing\Exception\PreConditionNotMetException('No task processing provider is installed that can handle this task type: ' . $task->getTaskTypeId());
1011
+        }
1012
+
1013
+        $provider = $this->getPreferredProvider($task->getTaskTypeId());
1014
+        if ($provider instanceof ISynchronousProvider) {
1015
+            $this->prepareTask($task);
1016
+            $task->setStatus(Task::STATUS_SCHEDULED);
1017
+            $this->storeTask($task);
1018
+            $this->processTask($task, $provider);
1019
+            $task = $this->getTask($task->getId());
1020
+        } else {
1021
+            $this->scheduleTask($task);
1022
+            // poll task
1023
+            while ($task->getStatus() === Task::STATUS_SCHEDULED || $task->getStatus() === Task::STATUS_RUNNING) {
1024
+                sleep(1);
1025
+                $task = $this->getTask($task->getId());
1026
+            }
1027
+        }
1028
+        return $task;
1029
+    }
1030
+
1031
+    public function processTask(Task $task, ISynchronousProvider $provider): bool {
1032
+        try {
1033
+            try {
1034
+                $input = $this->prepareInputData($task);
1035
+            } catch (GenericFileException|NotPermittedException|LockedException|ValidationException|UnauthorizedException $e) {
1036
+                $this->logger->warning('Failed to prepare input data for a TaskProcessing task with synchronous provider ' . $provider->getId(), ['exception' => $e]);
1037
+                $this->setTaskResult($task->getId(), $e->getMessage(), null);
1038
+                return false;
1039
+            }
1040
+            try {
1041
+                $this->setTaskStatus($task, Task::STATUS_RUNNING);
1042
+                $output = $provider->process($task->getUserId(), $input, fn (float $progress) => $this->setTaskProgress($task->getId(), $progress));
1043
+            } catch (ProcessingException $e) {
1044
+                $this->logger->warning('Failed to process a TaskProcessing task with synchronous provider ' . $provider->getId(), ['exception' => $e]);
1045
+                $userFacingErrorMessage = $e instanceof UserFacingProcessingException ? $e->getUserFacingMessage() : null;
1046
+                $this->setTaskResult($task->getId(), $e->getMessage(), null, userFacingError: $userFacingErrorMessage);
1047
+                return false;
1048
+            } catch (\Throwable $e) {
1049
+                $this->logger->error('Unknown error while processing TaskProcessing task', ['exception' => $e]);
1050
+                $this->setTaskResult($task->getId(), $e->getMessage(), null);
1051
+                return false;
1052
+            }
1053
+            $this->setTaskResult($task->getId(), null, $output);
1054
+        } catch (NotFoundException $e) {
1055
+            $this->logger->info('Could not find task anymore after execution. Moving on.', ['exception' => $e]);
1056
+        } catch (Exception $e) {
1057
+            $this->logger->error('Failed to report result of TaskProcessing task', ['exception' => $e]);
1058
+        }
1059
+        return true;
1060
+    }
1061
+
1062
+    public function deleteTask(Task $task): void {
1063
+        $taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
1064
+        $this->taskMapper->delete($taskEntity);
1065
+    }
1066
+
1067
+    public function getTask(int $id): Task {
1068
+        try {
1069
+            $taskEntity = $this->taskMapper->find($id);
1070
+            return $taskEntity->toPublicTask();
1071
+        } catch (DoesNotExistException $e) {
1072
+            throw new NotFoundException('Couldn\'t find task with id ' . $id, 0, $e);
1073
+        } catch (MultipleObjectsReturnedException|\OCP\DB\Exception $e) {
1074
+            throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding the task', 0, $e);
1075
+        } catch (\JsonException $e) {
1076
+            throw new \OCP\TaskProcessing\Exception\Exception('There was a problem parsing JSON after finding the task', 0, $e);
1077
+        }
1078
+    }
1079
+
1080
+    public function cancelTask(int $id): void {
1081
+        $task = $this->getTask($id);
1082
+        if ($task->getStatus() !== Task::STATUS_SCHEDULED && $task->getStatus() !== Task::STATUS_RUNNING) {
1083
+            return;
1084
+        }
1085
+        $task->setStatus(Task::STATUS_CANCELLED);
1086
+        $task->setEndedAt(time());
1087
+        $taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
1088
+        try {
1089
+            $this->taskMapper->update($taskEntity);
1090
+            $this->runWebhook($task);
1091
+        } catch (\OCP\DB\Exception $e) {
1092
+            throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding the task', 0, $e);
1093
+        }
1094
+    }
1095
+
1096
+    public function setTaskProgress(int $id, float $progress): bool {
1097
+        // TODO: Not sure if we should rather catch the exceptions of getTask here and fail silently
1098
+        $task = $this->getTask($id);
1099
+        if ($task->getStatus() === Task::STATUS_CANCELLED) {
1100
+            return false;
1101
+        }
1102
+        // only set the start time if the task is going from scheduled to running
1103
+        if ($task->getstatus() === Task::STATUS_SCHEDULED) {
1104
+            $task->setStartedAt(time());
1105
+        }
1106
+        $task->setStatus(Task::STATUS_RUNNING);
1107
+        $task->setProgress($progress);
1108
+        $taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
1109
+        try {
1110
+            $this->taskMapper->update($taskEntity);
1111
+        } catch (\OCP\DB\Exception $e) {
1112
+            throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding the task', 0, $e);
1113
+        }
1114
+        return true;
1115
+    }
1116
+
1117
+    public function setTaskResult(int $id, ?string $error, ?array $result, bool $isUsingFileIds = false, ?string $userFacingError = null): void {
1118
+        // TODO: Not sure if we should rather catch the exceptions of getTask here and fail silently
1119
+        $task = $this->getTask($id);
1120
+        if ($task->getStatus() === Task::STATUS_CANCELLED) {
1121
+            $this->logger->info('A TaskProcessing ' . $task->getTaskTypeId() . ' task with id ' . $id . ' finished but was cancelled in the mean time. Moving on without storing result.');
1122
+            return;
1123
+        }
1124
+        if ($error !== null) {
1125
+            $task->setStatus(Task::STATUS_FAILED);
1126
+            $task->setEndedAt(time());
1127
+            // truncate error message to 4000 characters
1128
+            $task->setErrorMessage(substr($error, 0, 4000));
1129
+            // truncate error message to 4000 characters
1130
+            if ($userFacingError !== null) {
1131
+                $task->setUserFacingErrorMessage(substr($userFacingError, 0, 4000));
1132
+            }
1133
+            $this->logger->warning('A TaskProcessing ' . $task->getTaskTypeId() . ' task with id ' . $id . ' failed with the following message: ' . $error);
1134
+        } elseif ($result !== null) {
1135
+            $taskTypes = $this->getAvailableTaskTypes();
1136
+            $outputShape = $taskTypes[$task->getTaskTypeId()]['outputShape'];
1137
+            $outputShapeEnumValues = $taskTypes[$task->getTaskTypeId()]['outputShapeEnumValues'];
1138
+            $optionalOutputShape = $taskTypes[$task->getTaskTypeId()]['optionalOutputShape'];
1139
+            $optionalOutputShapeEnumValues = $taskTypes[$task->getTaskTypeId()]['optionalOutputShapeEnumValues'];
1140
+            try {
1141
+                // validate output
1142
+                if (!$isUsingFileIds) {
1143
+                    $this->validateOutputWithFileData($outputShape, $outputShapeEnumValues, $result);
1144
+                    $this->validateOutputWithFileData($optionalOutputShape, $optionalOutputShapeEnumValues, $result, true);
1145
+                } else {
1146
+                    $this->validateOutputWithFileIds($outputShape, $outputShapeEnumValues, $result);
1147
+                    $this->validateOutputWithFileIds($optionalOutputShape, $optionalOutputShapeEnumValues, $result, true);
1148
+                }
1149
+                $output = $this->removeSuperfluousArrayKeys($result, $outputShape, $optionalOutputShape);
1150
+                // extract raw data and put it in files, replace it with file ids
1151
+                if (!$isUsingFileIds) {
1152
+                    $output = $this->encapsulateOutputFileData($output, $outputShape, $optionalOutputShape);
1153
+                } else {
1154
+                    $this->validateOutputFileIds($output, $outputShape, $optionalOutputShape);
1155
+                }
1156
+                // Turn file objects into IDs
1157
+                foreach ($output as $key => $value) {
1158
+                    if ($value instanceof Node) {
1159
+                        $output[$key] = $value->getId();
1160
+                    }
1161
+                    if (is_array($value) && isset($value[0]) && $value[0] instanceof Node) {
1162
+                        $output[$key] = array_map(fn ($node) => $node->getId(), $value);
1163
+                    }
1164
+                }
1165
+                $task->setOutput($output);
1166
+                $task->setProgress(1);
1167
+                $task->setStatus(Task::STATUS_SUCCESSFUL);
1168
+                $task->setEndedAt(time());
1169
+            } catch (ValidationException $e) {
1170
+                $task->setProgress(1);
1171
+                $task->setStatus(Task::STATUS_FAILED);
1172
+                $task->setEndedAt(time());
1173
+                $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';
1174
+                $task->setErrorMessage($error);
1175
+                $this->logger->error($error, ['exception' => $e, 'output' => $result]);
1176
+            } catch (NotPermittedException $e) {
1177
+                $task->setProgress(1);
1178
+                $task->setStatus(Task::STATUS_FAILED);
1179
+                $task->setEndedAt(time());
1180
+                $error = 'The task was processed successfully but storing the output in a file failed';
1181
+                $task->setErrorMessage($error);
1182
+                $this->logger->error($error, ['exception' => $e]);
1183
+            } catch (InvalidPathException|\OCP\Files\NotFoundException $e) {
1184
+                $task->setProgress(1);
1185
+                $task->setStatus(Task::STATUS_FAILED);
1186
+                $task->setEndedAt(time());
1187
+                $error = 'The task was processed successfully but the result file could not be found';
1188
+                $task->setErrorMessage($error);
1189
+                $this->logger->error($error, ['exception' => $e]);
1190
+            }
1191
+        }
1192
+        try {
1193
+            $taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
1194
+        } catch (\JsonException $e) {
1195
+            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);
1196
+        }
1197
+        try {
1198
+            $this->taskMapper->update($taskEntity);
1199
+            $this->runWebhook($task);
1200
+        } catch (\OCP\DB\Exception $e) {
1201
+            throw new \OCP\TaskProcessing\Exception\Exception($e->getMessage());
1202
+        }
1203
+        if ($task->getStatus() === Task::STATUS_SUCCESSFUL) {
1204
+            $event = new TaskSuccessfulEvent($task);
1205
+        } else {
1206
+            $event = new TaskFailedEvent($task, $error);
1207
+        }
1208
+        $this->dispatcher->dispatchTyped($event);
1209
+    }
1210
+
1211
+    public function getNextScheduledTask(array $taskTypeIds = [], array $taskIdsToIgnore = []): Task {
1212
+        try {
1213
+            $taskEntity = $this->taskMapper->findOldestScheduledByType($taskTypeIds, $taskIdsToIgnore);
1214
+            return $taskEntity->toPublicTask();
1215
+        } catch (DoesNotExistException $e) {
1216
+            throw new \OCP\TaskProcessing\Exception\NotFoundException('Could not find the task', previous: $e);
1217
+        } catch (\OCP\DB\Exception $e) {
1218
+            throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding the task', previous: $e);
1219
+        } catch (\JsonException $e) {
1220
+            throw new \OCP\TaskProcessing\Exception\Exception('There was a problem parsing JSON after finding the task', previous: $e);
1221
+        }
1222
+    }
1223
+
1224
+    public function getNextScheduledTasks(array $taskTypeIds = [], array $taskIdsToIgnore = [], int $numberOfTasks = 1): array {
1225
+        try {
1226
+            return array_map(fn ($taskEntity) => $taskEntity->toPublicTask(), $this->taskMapper->findNOldestScheduledByType($taskTypeIds, $taskIdsToIgnore, $numberOfTasks));
1227
+        } catch (DoesNotExistException $e) {
1228
+            throw new \OCP\TaskProcessing\Exception\NotFoundException('Could not find the task', previous: $e);
1229
+        } catch (\OCP\DB\Exception $e) {
1230
+            throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding the task', previous: $e);
1231
+        } catch (\JsonException $e) {
1232
+            throw new \OCP\TaskProcessing\Exception\Exception('There was a problem parsing JSON after finding the task', previous: $e);
1233
+        }
1234
+    }
1235
+
1236
+    /**
1237
+     * Takes task input data and replaces fileIds with File objects
1238
+     *
1239
+     * @param string|null $userId
1240
+     * @param array<array-key, list<numeric|string>|numeric|string> $input
1241
+     * @param ShapeDescriptor[] ...$specs the specs
1242
+     * @return array<array-key, list<File|numeric|string>|numeric|string|File>
1243
+     * @throws GenericFileException|LockedException|NotPermittedException|ValidationException|UnauthorizedException
1244
+     */
1245
+    public function fillInputFileData(?string $userId, array $input, ...$specs): array {
1246
+        if ($userId !== null) {
1247
+            \OC_Util::setupFS($userId);
1248
+        }
1249
+        $newInputOutput = [];
1250
+        $spec = array_reduce($specs, fn ($carry, $spec) => $carry + $spec, []);
1251
+        foreach ($spec as $key => $descriptor) {
1252
+            $type = $descriptor->getShapeType();
1253
+            if (!isset($input[$key])) {
1254
+                continue;
1255
+            }
1256
+            if (!in_array(EShapeType::getScalarType($type), [EShapeType::Image, EShapeType::Audio, EShapeType::Video, EShapeType::File], true)) {
1257
+                $newInputOutput[$key] = $input[$key];
1258
+                continue;
1259
+            }
1260
+            if (EShapeType::getScalarType($type) === $type) {
1261
+                // is scalar
1262
+                $node = $this->validateFileId((int)$input[$key]);
1263
+                $this->validateUserAccessToFile($input[$key], $userId);
1264
+                $newInputOutput[$key] = $node;
1265
+            } else {
1266
+                // is list
1267
+                $newInputOutput[$key] = [];
1268
+                foreach ($input[$key] as $item) {
1269
+                    $node = $this->validateFileId((int)$item);
1270
+                    $this->validateUserAccessToFile($item, $userId);
1271
+                    $newInputOutput[$key][] = $node;
1272
+                }
1273
+            }
1274
+        }
1275
+        return $newInputOutput;
1276
+    }
1277
+
1278
+    public function getUserTask(int $id, ?string $userId): Task {
1279
+        try {
1280
+            $taskEntity = $this->taskMapper->findByIdAndUser($id, $userId);
1281
+            return $taskEntity->toPublicTask();
1282
+        } catch (DoesNotExistException $e) {
1283
+            throw new \OCP\TaskProcessing\Exception\NotFoundException('Could not find the task', 0, $e);
1284
+        } catch (MultipleObjectsReturnedException|\OCP\DB\Exception $e) {
1285
+            throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding the task', 0, $e);
1286
+        } catch (\JsonException $e) {
1287
+            throw new \OCP\TaskProcessing\Exception\Exception('There was a problem parsing JSON after finding the task', 0, $e);
1288
+        }
1289
+    }
1290
+
1291
+    public function getUserTasks(?string $userId, ?string $taskTypeId = null, ?string $customId = null): array {
1292
+        try {
1293
+            $taskEntities = $this->taskMapper->findByUserAndTaskType($userId, $taskTypeId, $customId);
1294
+            return array_map(fn ($taskEntity): Task => $taskEntity->toPublicTask(), $taskEntities);
1295
+        } catch (\OCP\DB\Exception $e) {
1296
+            throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding the tasks', 0, $e);
1297
+        } catch (\JsonException $e) {
1298
+            throw new \OCP\TaskProcessing\Exception\Exception('There was a problem parsing JSON after finding the tasks', 0, $e);
1299
+        }
1300
+    }
1301
+
1302
+    public function getTasks(
1303
+        ?string $userId, ?string $taskTypeId = null, ?string $appId = null, ?string $customId = null,
1304
+        ?int $status = null, ?int $scheduleAfter = null, ?int $endedBefore = null,
1305
+    ): array {
1306
+        try {
1307
+            $taskEntities = $this->taskMapper->findTasks($userId, $taskTypeId, $appId, $customId, $status, $scheduleAfter, $endedBefore);
1308
+            return array_map(fn ($taskEntity): Task => $taskEntity->toPublicTask(), $taskEntities);
1309
+        } catch (\OCP\DB\Exception $e) {
1310
+            throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding the tasks', 0, $e);
1311
+        } catch (\JsonException $e) {
1312
+            throw new \OCP\TaskProcessing\Exception\Exception('There was a problem parsing JSON after finding the tasks', 0, $e);
1313
+        }
1314
+    }
1315
+
1316
+    public function getUserTasksByApp(?string $userId, string $appId, ?string $customId = null): array {
1317
+        try {
1318
+            $taskEntities = $this->taskMapper->findUserTasksByApp($userId, $appId, $customId);
1319
+            return array_map(fn ($taskEntity): Task => $taskEntity->toPublicTask(), $taskEntities);
1320
+        } catch (\OCP\DB\Exception $e) {
1321
+            throw new \OCP\TaskProcessing\Exception\Exception('There was a problem finding a task', 0, $e);
1322
+        } catch (\JsonException $e) {
1323
+            throw new \OCP\TaskProcessing\Exception\Exception('There was a problem parsing JSON after finding a task', 0, $e);
1324
+        }
1325
+    }
1326
+
1327
+    /**
1328
+     *Takes task input or output and replaces base64 data with file ids
1329
+     *
1330
+     * @param array $output
1331
+     * @param ShapeDescriptor[] ...$specs the specs that define which keys to keep
1332
+     * @return array
1333
+     * @throws NotPermittedException
1334
+     */
1335
+    public function encapsulateOutputFileData(array $output, ...$specs): array {
1336
+        $newOutput = [];
1337
+        try {
1338
+            $folder = $this->appData->getFolder('TaskProcessing');
1339
+        } catch (\OCP\Files\NotFoundException) {
1340
+            $folder = $this->appData->newFolder('TaskProcessing');
1341
+        }
1342
+        $spec = array_reduce($specs, fn ($carry, $spec) => $carry + $spec, []);
1343
+        foreach ($spec as $key => $descriptor) {
1344
+            $type = $descriptor->getShapeType();
1345
+            if (!isset($output[$key])) {
1346
+                continue;
1347
+            }
1348
+            if (!in_array(EShapeType::getScalarType($type), [EShapeType::Image, EShapeType::Audio, EShapeType::Video, EShapeType::File], true)) {
1349
+                $newOutput[$key] = $output[$key];
1350
+                continue;
1351
+            }
1352
+            if (EShapeType::getScalarType($type) === $type) {
1353
+                /** @var SimpleFile $file */
1354
+                $file = $folder->newFile(time() . '-' . rand(1, 100000), $output[$key]);
1355
+                $newOutput[$key] = $file->getId(); // polymorphic call to SimpleFile
1356
+            } else {
1357
+                $newOutput = [];
1358
+                foreach ($output[$key] as $item) {
1359
+                    /** @var SimpleFile $file */
1360
+                    $file = $folder->newFile(time() . '-' . rand(1, 100000), $item);
1361
+                    $newOutput[$key][] = $file->getId();
1362
+                }
1363
+            }
1364
+        }
1365
+        return $newOutput;
1366
+    }
1367
+
1368
+    /**
1369
+     * @param Task $task
1370
+     * @return array<array-key, list<numeric|string|File>|numeric|string|File>
1371
+     * @throws GenericFileException
1372
+     * @throws LockedException
1373
+     * @throws NotPermittedException
1374
+     * @throws ValidationException|UnauthorizedException
1375
+     */
1376
+    public function prepareInputData(Task $task): array {
1377
+        $taskTypes = $this->getAvailableTaskTypes();
1378
+        $inputShape = $taskTypes[$task->getTaskTypeId()]['inputShape'];
1379
+        $optionalInputShape = $taskTypes[$task->getTaskTypeId()]['optionalInputShape'];
1380
+        $input = $task->getInput();
1381
+        $input = $this->removeSuperfluousArrayKeys($input, $inputShape, $optionalInputShape);
1382
+        $input = $this->fillInputFileData($task->getUserId(), $input, $inputShape, $optionalInputShape);
1383
+        return $input;
1384
+    }
1385
+
1386
+    public function lockTask(Task $task): bool {
1387
+        $taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
1388
+        if ($this->taskMapper->lockTask($taskEntity) === 0) {
1389
+            return false;
1390
+        }
1391
+        $task->setStatus(Task::STATUS_RUNNING);
1392
+        return true;
1393
+    }
1394
+
1395
+    /**
1396
+     * @throws \JsonException
1397
+     * @throws Exception
1398
+     */
1399
+    public function setTaskStatus(Task $task, int $status): void {
1400
+        $currentTaskStatus = $task->getStatus();
1401
+        if ($currentTaskStatus === Task::STATUS_SCHEDULED && $status === Task::STATUS_RUNNING) {
1402
+            $task->setStartedAt(time());
1403
+        } elseif ($currentTaskStatus === Task::STATUS_RUNNING && ($status === Task::STATUS_FAILED || $status === Task::STATUS_CANCELLED)) {
1404
+            $task->setEndedAt(time());
1405
+        } elseif ($currentTaskStatus === Task::STATUS_UNKNOWN && $status === Task::STATUS_SCHEDULED) {
1406
+            $task->setScheduledAt(time());
1407
+        }
1408
+        $task->setStatus($status);
1409
+        $taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
1410
+        $this->taskMapper->update($taskEntity);
1411
+    }
1412
+
1413
+    /**
1414
+     * Validate input, fill input default values, set completionExpectedAt, set scheduledAt
1415
+     *
1416
+     * @param Task $task
1417
+     * @return void
1418
+     * @throws UnauthorizedException
1419
+     * @throws ValidationException
1420
+     * @throws \OCP\TaskProcessing\Exception\Exception
1421
+     */
1422
+    private function prepareTask(Task $task): void {
1423
+        $taskTypes = $this->getAvailableTaskTypes();
1424
+        $taskType = $taskTypes[$task->getTaskTypeId()];
1425
+        $inputShape = $taskType['inputShape'];
1426
+        $inputShapeDefaults = $taskType['inputShapeDefaults'];
1427
+        $inputShapeEnumValues = $taskType['inputShapeEnumValues'];
1428
+        $optionalInputShape = $taskType['optionalInputShape'];
1429
+        $optionalInputShapeEnumValues = $taskType['optionalInputShapeEnumValues'];
1430
+        $optionalInputShapeDefaults = $taskType['optionalInputShapeDefaults'];
1431
+        // validate input
1432
+        $this->validateInput($inputShape, $inputShapeDefaults, $inputShapeEnumValues, $task->getInput());
1433
+        $this->validateInput($optionalInputShape, $optionalInputShapeDefaults, $optionalInputShapeEnumValues, $task->getInput(), true);
1434
+        // authenticate access to mentioned files
1435
+        $ids = [];
1436
+        foreach ($inputShape + $optionalInputShape as $key => $descriptor) {
1437
+            if (in_array(EShapeType::getScalarType($descriptor->getShapeType()), [EShapeType::File, EShapeType::Image, EShapeType::Audio, EShapeType::Video], true)) {
1438
+                /** @var list<int>|int $inputSlot */
1439
+                $inputSlot = $task->getInput()[$key];
1440
+                if (is_array($inputSlot)) {
1441
+                    $ids += $inputSlot;
1442
+                } else {
1443
+                    $ids[] = $inputSlot;
1444
+                }
1445
+            }
1446
+        }
1447
+        foreach ($ids as $fileId) {
1448
+            $this->validateFileId($fileId);
1449
+            $this->validateUserAccessToFile($fileId, $task->getUserId());
1450
+        }
1451
+        // remove superfluous keys and set input
1452
+        $input = $this->removeSuperfluousArrayKeys($task->getInput(), $inputShape, $optionalInputShape);
1453
+        $inputWithDefaults = $this->fillInputDefaults($input, $inputShapeDefaults, $optionalInputShapeDefaults);
1454
+        $task->setInput($inputWithDefaults);
1455
+        $task->setScheduledAt(time());
1456
+        $provider = $this->getPreferredProvider($task->getTaskTypeId());
1457
+        // calculate expected completion time
1458
+        $completionExpectedAt = new \DateTime('now');
1459
+        $completionExpectedAt->add(new \DateInterval('PT' . $provider->getExpectedRuntime() . 'S'));
1460
+        $task->setCompletionExpectedAt($completionExpectedAt);
1461
+    }
1462
+
1463
+    /**
1464
+     * Store the task in the DB and set its ID in the \OCP\TaskProcessing\Task input param
1465
+     *
1466
+     * @param Task $task
1467
+     * @return void
1468
+     * @throws Exception
1469
+     * @throws \JsonException
1470
+     */
1471
+    private function storeTask(Task $task): void {
1472
+        // create a db entity and insert into db table
1473
+        $taskEntity = \OC\TaskProcessing\Db\Task::fromPublicTask($task);
1474
+        $this->taskMapper->insert($taskEntity);
1475
+        // make sure the scheduler knows the id
1476
+        $task->setId($taskEntity->getId());
1477
+    }
1478
+
1479
+    /**
1480
+     * @param array $output
1481
+     * @param ShapeDescriptor[] ...$specs the specs that define which keys to keep
1482
+     * @return array
1483
+     * @throws NotPermittedException
1484
+     */
1485
+    private function validateOutputFileIds(array $output, ...$specs): array {
1486
+        $newOutput = [];
1487
+        $spec = array_reduce($specs, fn ($carry, $spec) => $carry + $spec, []);
1488
+        foreach ($spec as $key => $descriptor) {
1489
+            $type = $descriptor->getShapeType();
1490
+            if (!isset($output[$key])) {
1491
+                continue;
1492
+            }
1493
+            if (!in_array(EShapeType::getScalarType($type), [EShapeType::Image, EShapeType::Audio, EShapeType::Video, EShapeType::File], true)) {
1494
+                $newOutput[$key] = $output[$key];
1495
+                continue;
1496
+            }
1497
+            if (EShapeType::getScalarType($type) === $type) {
1498
+                // Is scalar file ID
1499
+                $newOutput[$key] = $this->validateFileId($output[$key]);
1500
+            } else {
1501
+                // Is list of file IDs
1502
+                $newOutput = [];
1503
+                foreach ($output[$key] as $item) {
1504
+                    $newOutput[$key][] = $this->validateFileId($item);
1505
+                }
1506
+            }
1507
+        }
1508
+        return $newOutput;
1509
+    }
1510
+
1511
+    /**
1512
+     * @param mixed $id
1513
+     * @return File
1514
+     * @throws ValidationException
1515
+     */
1516
+    private function validateFileId(mixed $id): File {
1517
+        $node = $this->rootFolder->getFirstNodeById($id);
1518
+        if ($node === null) {
1519
+            $node = $this->rootFolder->getFirstNodeByIdInPath($id, '/' . $this->rootFolder->getAppDataDirectoryName() . '/');
1520
+            if ($node === null) {
1521
+                throw new ValidationException('Could not find file ' . $id);
1522
+            } elseif (!$node instanceof File) {
1523
+                throw new ValidationException('File with id "' . $id . '" is not a file');
1524
+            }
1525
+        } elseif (!$node instanceof File) {
1526
+            throw new ValidationException('File with id "' . $id . '" is not a file');
1527
+        }
1528
+        return $node;
1529
+    }
1530
+
1531
+    /**
1532
+     * @param mixed $fileId
1533
+     * @param string|null $userId
1534
+     * @return void
1535
+     * @throws UnauthorizedException
1536
+     */
1537
+    private function validateUserAccessToFile(mixed $fileId, ?string $userId): void {
1538
+        if ($userId === null) {
1539
+            throw new UnauthorizedException('User does not have access to file ' . $fileId);
1540
+        }
1541
+        $mounts = $this->userMountCache->getMountsForFileId($fileId);
1542
+        $userIds = array_map(fn ($mount) => $mount->getUser()->getUID(), $mounts);
1543
+        if (!in_array($userId, $userIds)) {
1544
+            throw new UnauthorizedException('User ' . $userId . ' does not have access to file ' . $fileId);
1545
+        }
1546
+    }
1547
+
1548
+    /**
1549
+     * @param Task $task
1550
+     * @return list<int>
1551
+     * @throws NotFoundException
1552
+     */
1553
+    public function extractFileIdsFromTask(Task $task): array {
1554
+        $ids = [];
1555
+        $taskTypes = $this->getAvailableTaskTypes();
1556
+        if (!isset($taskTypes[$task->getTaskTypeId()])) {
1557
+            throw new NotFoundException('Could not find task type');
1558
+        }
1559
+        $taskType = $taskTypes[$task->getTaskTypeId()];
1560
+        foreach ($taskType['inputShape'] + $taskType['optionalInputShape'] as $key => $descriptor) {
1561
+            if (in_array(EShapeType::getScalarType($descriptor->getShapeType()), [EShapeType::File, EShapeType::Image, EShapeType::Audio, EShapeType::Video], true)) {
1562
+                /** @var int|list<int> $inputSlot */
1563
+                $inputSlot = $task->getInput()[$key];
1564
+                if (is_array($inputSlot)) {
1565
+                    $ids = array_merge($inputSlot, $ids);
1566
+                } else {
1567
+                    $ids[] = $inputSlot;
1568
+                }
1569
+            }
1570
+        }
1571
+        if ($task->getOutput() !== null) {
1572
+            foreach ($taskType['outputShape'] + $taskType['optionalOutputShape'] as $key => $descriptor) {
1573
+                if (in_array(EShapeType::getScalarType($descriptor->getShapeType()), [EShapeType::File, EShapeType::Image, EShapeType::Audio, EShapeType::Video], true)) {
1574
+                    /** @var int|list<int> $outputSlot */
1575
+                    $outputSlot = $task->getOutput()[$key];
1576
+                    if (is_array($outputSlot)) {
1577
+                        $ids = array_merge($outputSlot, $ids);
1578
+                    } else {
1579
+                        $ids[] = $outputSlot;
1580
+                    }
1581
+                }
1582
+            }
1583
+        }
1584
+        return $ids;
1585
+    }
1586
+
1587
+    /**
1588
+     * @param ISimpleFolder $folder
1589
+     * @param int $ageInSeconds
1590
+     * @return \Generator
1591
+     */
1592
+    public function clearFilesOlderThan(ISimpleFolder $folder, int $ageInSeconds = self::MAX_TASK_AGE_SECONDS): \Generator {
1593
+        foreach ($folder->getDirectoryListing() as $file) {
1594
+            if ($file->getMTime() < time() - $ageInSeconds) {
1595
+                try {
1596
+                    $fileName = $file->getName();
1597
+                    $file->delete();
1598
+                    yield $fileName;
1599
+                } catch (NotPermittedException $e) {
1600
+                    $this->logger->warning('Failed to delete a stale task processing file', ['exception' => $e]);
1601
+                }
1602
+            }
1603
+        }
1604
+    }
1605
+
1606
+    /**
1607
+     * @param int $ageInSeconds
1608
+     * @return \Generator
1609
+     * @throws Exception
1610
+     * @throws InvalidPathException
1611
+     * @throws NotFoundException
1612
+     * @throws \JsonException
1613
+     * @throws \OCP\Files\NotFoundException
1614
+     */
1615
+    public function cleanupTaskProcessingTaskFiles(int $ageInSeconds = self::MAX_TASK_AGE_SECONDS): \Generator {
1616
+        $taskIdsToCleanup = [];
1617
+        foreach ($this->taskMapper->getTasksToCleanup($ageInSeconds) as $task) {
1618
+            $taskIdsToCleanup[] = $task->getId();
1619
+            $ocpTask = $task->toPublicTask();
1620
+            $fileIds = $this->extractFileIdsFromTask($ocpTask);
1621
+            foreach ($fileIds as $fileId) {
1622
+                // only look for output files stored in appData/TaskProcessing/
1623
+                $file = $this->rootFolder->getFirstNodeByIdInPath($fileId, '/' . $this->rootFolder->getAppDataDirectoryName() . '/core/TaskProcessing/');
1624
+                if ($file instanceof File) {
1625
+                    try {
1626
+                        $fileId = $file->getId();
1627
+                        $fileName = $file->getName();
1628
+                        $file->delete();
1629
+                        yield ['task_id' => $task->getId(), 'file_id' => $fileId, 'file_name' => $fileName];
1630
+                    } catch (NotPermittedException $e) {
1631
+                        $this->logger->warning('Failed to delete a stale task processing file', ['exception' => $e]);
1632
+                    }
1633
+                }
1634
+            }
1635
+        }
1636
+        return $taskIdsToCleanup;
1637
+    }
1638
+
1639
+    /**
1640
+     * Make a request to the task's webhookUri if necessary
1641
+     *
1642
+     * @param Task $task
1643
+     */
1644
+    private function runWebhook(Task $task): void {
1645
+        $uri = $task->getWebhookUri();
1646
+        $method = $task->getWebhookMethod();
1647
+
1648
+        if (!$uri || !$method) {
1649
+            return;
1650
+        }
1651
+
1652
+        if (in_array($method, ['HTTP:GET', 'HTTP:POST', 'HTTP:PUT', 'HTTP:DELETE'], true)) {
1653
+            $client = $this->clientService->newClient();
1654
+            $httpMethod = preg_replace('/^HTTP:/', '', $method);
1655
+            $options = [
1656
+                'timeout' => 30,
1657
+                'body' => json_encode([
1658
+                    'task' => $task->jsonSerialize(),
1659
+                ]),
1660
+                'headers' => ['Content-Type' => 'application/json'],
1661
+            ];
1662
+            try {
1663
+                $client->request($httpMethod, $uri, $options);
1664
+            } catch (ClientException|ServerException $e) {
1665
+                $this->logger->warning('Task processing HTTP webhook failed for task ' . $task->getId() . '. Request failed', ['exception' => $e]);
1666
+            } catch (\Exception|\Throwable $e) {
1667
+                $this->logger->warning('Task processing HTTP webhook failed for task ' . $task->getId() . '. Unknown error', ['exception' => $e]);
1668
+            }
1669
+        } elseif (str_starts_with($method, 'AppAPI:') && str_starts_with($uri, '/')) {
1670
+            $parsedMethod = explode(':', $method, 4);
1671
+            if (count($parsedMethod) < 3) {
1672
+                $this->logger->warning('Task processing AppAPI webhook failed for task ' . $task->getId() . '. Invalid method: ' . $method);
1673
+            }
1674
+            [, $exAppId, $httpMethod] = $parsedMethod;
1675
+            if (!$this->appManager->isEnabledForAnyone('app_api')) {
1676
+                $this->logger->warning('Task processing AppAPI webhook failed for task ' . $task->getId() . '. AppAPI is disabled or not installed.');
1677
+                return;
1678
+            }
1679
+            try {
1680
+                $appApiFunctions = \OCP\Server::get(\OCA\AppAPI\PublicFunctions::class);
1681
+            } catch (ContainerExceptionInterface|NotFoundExceptionInterface) {
1682
+                $this->logger->warning('Task processing AppAPI webhook failed for task ' . $task->getId() . '. Could not get AppAPI public functions.');
1683
+                return;
1684
+            }
1685
+            $exApp = $appApiFunctions->getExApp($exAppId);
1686
+            if ($exApp === null) {
1687
+                $this->logger->warning('Task processing AppAPI webhook failed for task ' . $task->getId() . '. ExApp ' . $exAppId . ' is missing.');
1688
+                return;
1689
+            } elseif (!$exApp['enabled']) {
1690
+                $this->logger->warning('Task processing AppAPI webhook failed for task ' . $task->getId() . '. ExApp ' . $exAppId . ' is disabled.');
1691
+                return;
1692
+            }
1693
+            $requestParams = [
1694
+                'task' => $task->jsonSerialize(),
1695
+            ];
1696
+            $requestOptions = [
1697
+                'timeout' => 30,
1698
+            ];
1699
+            $response = $appApiFunctions->exAppRequest($exAppId, $uri, $task->getUserId(), $httpMethod, $requestParams, $requestOptions);
1700
+            if (is_array($response) && isset($response['error'])) {
1701
+                $this->logger->warning('Task processing AppAPI webhook failed for task ' . $task->getId() . '. Error during request to ExApp(' . $exAppId . '): ', $response['error']);
1702
+            }
1703
+        }
1704
+    }
1705 1705
 }
Please login to merge, or discard this patch.
core/Migrations/Version33000Date20251013110519.php 1 patch
Indentation   +21 added lines, -21 removed lines patch added patch discarded remove patch
@@ -22,27 +22,27 @@
 block discarded – undo
22 22
 #[AddColumn(table: 'taskprocessing_tasks', name: 'user_facing_error_message', type: ColumnType::STRING)]
23 23
 class Version33000Date20251013110519 extends SimpleMigrationStep {
24 24
 
25
-	/**
26
-	 * @param IOutput $output
27
-	 * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
28
-	 * @param array $options
29
-	 * @return null|ISchemaWrapper
30
-	 */
31
-	public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
32
-		/** @var ISchemaWrapper $schema */
33
-		$schema = $schemaClosure();
25
+    /**
26
+     * @param IOutput $output
27
+     * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
28
+     * @param array $options
29
+     * @return null|ISchemaWrapper
30
+     */
31
+    public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
32
+        /** @var ISchemaWrapper $schema */
33
+        $schema = $schemaClosure();
34 34
 
35
-		if ($schema->hasTable('taskprocessing_tasks')) {
36
-			$table = $schema->getTable('taskprocessing_tasks');
37
-			if (!$table->hasColumn('user_facing_error_message')) {
38
-				$table->addColumn('user_facing_error_message', Types::STRING, [
39
-					'notnull' => false,
40
-					'length' => 4000,
41
-				]);
42
-				return $schema;
43
-			}
44
-		}
35
+        if ($schema->hasTable('taskprocessing_tasks')) {
36
+            $table = $schema->getTable('taskprocessing_tasks');
37
+            if (!$table->hasColumn('user_facing_error_message')) {
38
+                $table->addColumn('user_facing_error_message', Types::STRING, [
39
+                    'notnull' => false,
40
+                    'length' => 4000,
41
+                ]);
42
+                return $schema;
43
+            }
44
+        }
45 45
 
46
-		return null;
47
-	}
46
+        return null;
47
+    }
48 48
 }
Please login to merge, or discard this patch.
core/Controller/TaskProcessingApiController.php 1 patch
Indentation   +632 added lines, -632 removed lines patch added patch discarded remove patch
@@ -45,636 +45,636 @@
 block discarded – undo
45 45
  * @psalm-import-type CoreTaskProcessingTaskType from ResponseDefinitions
46 46
  */
47 47
 class TaskProcessingApiController extends OCSController {
48
-	public function __construct(
49
-		string $appName,
50
-		IRequest $request,
51
-		private IManager $taskProcessingManager,
52
-		private IL10N $l,
53
-		private ?string $userId,
54
-		private IRootFolder $rootFolder,
55
-		private IAppData $appData,
56
-		private IMimeTypeDetector $mimeTypeDetector,
57
-	) {
58
-		parent::__construct($appName, $request);
59
-	}
60
-
61
-	/**
62
-	 * Returns all available TaskProcessing task types
63
-	 *
64
-	 * @return DataResponse<Http::STATUS_OK, array{types: array<string, CoreTaskProcessingTaskType>}, array{}>
65
-	 *
66
-	 * 200: Task types returned
67
-	 */
68
-	#[NoAdminRequired]
69
-	#[ApiRoute(verb: 'GET', url: '/tasktypes', root: '/taskprocessing')]
70
-	public function taskTypes(): DataResponse {
71
-		/** @var array<string, CoreTaskProcessingTaskType> $taskTypes */
72
-		$taskTypes = array_map(function (array $tt) {
73
-			$tt['inputShape'] = array_map(function ($descriptor) {
74
-				return $descriptor->jsonSerialize();
75
-			}, $tt['inputShape']);
76
-			if (empty($tt['inputShape'])) {
77
-				$tt['inputShape'] = new stdClass;
78
-			}
79
-
80
-			$tt['outputShape'] = array_map(function ($descriptor) {
81
-				return $descriptor->jsonSerialize();
82
-			}, $tt['outputShape']);
83
-			if (empty($tt['outputShape'])) {
84
-				$tt['outputShape'] = new stdClass;
85
-			}
86
-
87
-			$tt['optionalInputShape'] = array_map(function ($descriptor) {
88
-				return $descriptor->jsonSerialize();
89
-			}, $tt['optionalInputShape']);
90
-			if (empty($tt['optionalInputShape'])) {
91
-				$tt['optionalInputShape'] = new stdClass;
92
-			}
93
-
94
-			$tt['optionalOutputShape'] = array_map(function ($descriptor) {
95
-				return $descriptor->jsonSerialize();
96
-			}, $tt['optionalOutputShape']);
97
-			if (empty($tt['optionalOutputShape'])) {
98
-				$tt['optionalOutputShape'] = new stdClass;
99
-			}
100
-
101
-			$tt['inputShapeEnumValues'] = array_map(function (array $enumValues) {
102
-				return array_map(fn (ShapeEnumValue $enumValue) => $enumValue->jsonSerialize(), $enumValues);
103
-			}, $tt['inputShapeEnumValues']);
104
-			if (empty($tt['inputShapeEnumValues'])) {
105
-				$tt['inputShapeEnumValues'] = new stdClass;
106
-			}
107
-
108
-			$tt['optionalInputShapeEnumValues'] = array_map(function (array $enumValues) {
109
-				return array_map(fn (ShapeEnumValue $enumValue) => $enumValue->jsonSerialize(), $enumValues);
110
-			}, $tt['optionalInputShapeEnumValues']);
111
-			if (empty($tt['optionalInputShapeEnumValues'])) {
112
-				$tt['optionalInputShapeEnumValues'] = new stdClass;
113
-			}
114
-
115
-			$tt['outputShapeEnumValues'] = array_map(function (array $enumValues) {
116
-				return array_map(fn (ShapeEnumValue $enumValue) => $enumValue->jsonSerialize(), $enumValues);
117
-			}, $tt['outputShapeEnumValues']);
118
-			if (empty($tt['outputShapeEnumValues'])) {
119
-				$tt['outputShapeEnumValues'] = new stdClass;
120
-			}
121
-
122
-			$tt['optionalOutputShapeEnumValues'] = array_map(function (array $enumValues) {
123
-				return array_map(fn (ShapeEnumValue $enumValue) => $enumValue->jsonSerialize(), $enumValues);
124
-			}, $tt['optionalOutputShapeEnumValues']);
125
-			if (empty($tt['optionalOutputShapeEnumValues'])) {
126
-				$tt['optionalOutputShapeEnumValues'] = new stdClass;
127
-			}
128
-
129
-			if (empty($tt['inputShapeDefaults'])) {
130
-				$tt['inputShapeDefaults'] = new stdClass;
131
-			}
132
-			if (empty($tt['optionalInputShapeDefaults'])) {
133
-				$tt['optionalInputShapeDefaults'] = new stdClass;
134
-			}
135
-			return $tt;
136
-		}, $this->taskProcessingManager->getAvailableTaskTypes());
137
-		return new DataResponse([
138
-			'types' => $taskTypes,
139
-		]);
140
-	}
141
-
142
-	/**
143
-	 * Schedules a task
144
-	 *
145
-	 * @param array<string, mixed> $input Task's input parameters
146
-	 * @param string $type Type of the task
147
-	 * @param string $appId ID of the app that will execute the task
148
-	 * @param string $customId An arbitrary identifier for the task
149
-	 * @param string|null $webhookUri URI to be requested when the task finishes
150
-	 * @param string|null $webhookMethod Method used for the webhook request (HTTP:GET, HTTP:POST, HTTP:PUT, HTTP:DELETE or AppAPI:APP_ID:GET, AppAPI:APP_ID:POST...)
151
-	 * @return DataResponse<Http::STATUS_OK, array{task: CoreTaskProcessingTask}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_BAD_REQUEST|Http::STATUS_PRECONDITION_FAILED|Http::STATUS_UNAUTHORIZED, array{message: string}, array{}>
152
-	 *
153
-	 * 200: Task scheduled successfully
154
-	 * 400: Scheduling task is not possible
155
-	 * 412: Scheduling task is not possible
156
-	 * 401: Cannot schedule task because it references files in its input that the user doesn't have access to
157
-	 */
158
-	#[UserRateLimit(limit: 20, period: 120)]
159
-	#[NoAdminRequired]
160
-	#[ApiRoute(verb: 'POST', url: '/schedule', root: '/taskprocessing')]
161
-	public function schedule(
162
-		array $input, string $type, string $appId, string $customId = '',
163
-		?string $webhookUri = null, ?string $webhookMethod = null,
164
-	): DataResponse {
165
-		$task = new Task($type, $input, $appId, $this->userId, $customId);
166
-		$task->setWebhookUri($webhookUri);
167
-		$task->setWebhookMethod($webhookMethod);
168
-		try {
169
-			$this->taskProcessingManager->scheduleTask($task);
170
-
171
-			/** @var CoreTaskProcessingTask $json */
172
-			$json = $task->jsonSerialize();
173
-
174
-			return new DataResponse([
175
-				'task' => $json,
176
-			]);
177
-		} catch (PreConditionNotMetException) {
178
-			return new DataResponse(['message' => $this->l->t('The given provider is not available')], Http::STATUS_PRECONDITION_FAILED);
179
-		} catch (ValidationException $e) {
180
-			return new DataResponse(['message' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
181
-		} catch (UnauthorizedException) {
182
-			return new DataResponse(['message' => 'User does not have access to the files mentioned in the task input'], Http::STATUS_UNAUTHORIZED);
183
-		} catch (Exception) {
184
-			return new DataResponse(['message' => 'Internal server error'], Http::STATUS_INTERNAL_SERVER_ERROR);
185
-		}
186
-	}
187
-
188
-	/**
189
-	 * Gets a task including status and result
190
-	 *
191
-	 * Tasks are removed 1 week after receiving their last update
192
-	 *
193
-	 * @param int $id The id of the task
194
-	 *
195
-	 * @return DataResponse<Http::STATUS_OK, array{task: CoreTaskProcessingTask}, array{}>|DataResponse<Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
196
-	 *
197
-	 * 200: Task returned
198
-	 * 404: Task not found
199
-	 */
200
-	#[NoAdminRequired]
201
-	#[ApiRoute(verb: 'GET', url: '/task/{id}', root: '/taskprocessing')]
202
-	public function getTask(int $id): DataResponse {
203
-		try {
204
-			$task = $this->taskProcessingManager->getUserTask($id, $this->userId);
205
-
206
-			/** @var CoreTaskProcessingTask $json */
207
-			$json = $task->jsonSerialize();
208
-
209
-			return new DataResponse([
210
-				'task' => $json,
211
-			]);
212
-		} catch (NotFoundException) {
213
-			return new DataResponse(['message' => $this->l->t('Task not found')], Http::STATUS_NOT_FOUND);
214
-		} catch (RuntimeException) {
215
-			return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
216
-		}
217
-	}
218
-
219
-	/**
220
-	 * Deletes a task
221
-	 *
222
-	 * @param int $id The id of the task
223
-	 *
224
-	 * @return DataResponse<Http::STATUS_OK, null, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
225
-	 *
226
-	 * 200: Task deleted
227
-	 */
228
-	#[NoAdminRequired]
229
-	#[ApiRoute(verb: 'DELETE', url: '/task/{id}', root: '/taskprocessing')]
230
-	public function deleteTask(int $id): DataResponse {
231
-		try {
232
-			$task = $this->taskProcessingManager->getUserTask($id, $this->userId);
233
-
234
-			$this->taskProcessingManager->deleteTask($task);
235
-
236
-			return new DataResponse(null);
237
-		} catch (NotFoundException) {
238
-			return new DataResponse(null);
239
-		} catch (Exception) {
240
-			return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
241
-		}
242
-	}
243
-
244
-
245
-	/**
246
-	 * Returns tasks for the current user filtered by the appId and optional customId
247
-	 *
248
-	 * @param string $appId ID of the app
249
-	 * @param string|null $customId An arbitrary identifier for the task
250
-	 * @return DataResponse<Http::STATUS_OK, array{tasks: list<CoreTaskProcessingTask>}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
251
-	 *
252
-	 * 200: Tasks returned
253
-	 */
254
-	#[NoAdminRequired]
255
-	#[ApiRoute(verb: 'GET', url: '/tasks/app/{appId}', root: '/taskprocessing')]
256
-	public function listTasksByApp(string $appId, ?string $customId = null): DataResponse {
257
-		try {
258
-			$tasks = $this->taskProcessingManager->getUserTasksByApp($this->userId, $appId, $customId);
259
-			$json = array_map(static function (Task $task) {
260
-				return $task->jsonSerialize();
261
-			}, $tasks);
262
-
263
-			return new DataResponse([
264
-				'tasks' => $json,
265
-			]);
266
-		} catch (Exception) {
267
-			return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
268
-		}
269
-	}
270
-
271
-	/**
272
-	 * Returns tasks for the current user filtered by the optional taskType and optional customId
273
-	 *
274
-	 * @param string|null $taskType The task type to filter by
275
-	 * @param string|null $customId An arbitrary identifier for the task
276
-	 * @return DataResponse<Http::STATUS_OK, array{tasks: list<CoreTaskProcessingTask>}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
277
-	 *
278
-	 * 200: Tasks returned
279
-	 */
280
-	#[NoAdminRequired]
281
-	#[ApiRoute(verb: 'GET', url: '/tasks', root: '/taskprocessing')]
282
-	public function listTasks(?string $taskType, ?string $customId = null): DataResponse {
283
-		try {
284
-			$tasks = $this->taskProcessingManager->getUserTasks($this->userId, $taskType, $customId);
285
-			$json = array_map(static function (Task $task) {
286
-				return $task->jsonSerialize();
287
-			}, $tasks);
288
-
289
-			return new DataResponse([
290
-				'tasks' => $json,
291
-			]);
292
-		} catch (Exception) {
293
-			return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
294
-		}
295
-	}
296
-
297
-	/**
298
-	 * Returns the contents of a file referenced in a task
299
-	 *
300
-	 * @param int $taskId The id of the task
301
-	 * @param int $fileId The file id of the file to retrieve
302
-	 * @return StreamResponse<Http::STATUS_OK, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}>
303
-	 *
304
-	 * 200: File content returned
305
-	 * 404: Task or file not found
306
-	 */
307
-	#[NoAdminRequired]
308
-	#[NoCSRFRequired]
309
-	#[ApiRoute(verb: 'GET', url: '/tasks/{taskId}/file/{fileId}', root: '/taskprocessing')]
310
-	public function getFileContents(int $taskId, int $fileId): StreamResponse|DataResponse {
311
-		try {
312
-			$task = $this->taskProcessingManager->getUserTask($taskId, $this->userId);
313
-			return $this->getFileContentsInternal($task, $fileId);
314
-		} catch (NotFoundException) {
315
-			return new DataResponse(['message' => $this->l->t('Not found')], Http::STATUS_NOT_FOUND);
316
-		} catch (LockedException) {
317
-			return new DataResponse(['message' => $this->l->t('Node is locked')], Http::STATUS_INTERNAL_SERVER_ERROR);
318
-		} catch (Exception) {
319
-			return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
320
-		}
321
-	}
322
-
323
-	/**
324
-	 * Returns the contents of a file referenced in a task(ExApp route version)
325
-	 *
326
-	 * @param int $taskId The id of the task
327
-	 * @param int $fileId The file id of the file to retrieve
328
-	 * @return StreamResponse<Http::STATUS_OK, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}>
329
-	 *
330
-	 * 200: File content returned
331
-	 * 404: Task or file not found
332
-	 */
333
-	#[ExAppRequired]
334
-	#[ApiRoute(verb: 'GET', url: '/tasks_provider/{taskId}/file/{fileId}', root: '/taskprocessing')]
335
-	public function getFileContentsExApp(int $taskId, int $fileId): StreamResponse|DataResponse {
336
-		try {
337
-			$task = $this->taskProcessingManager->getTask($taskId);
338
-			return $this->getFileContentsInternal($task, $fileId);
339
-		} catch (NotFoundException) {
340
-			return new DataResponse(['message' => $this->l->t('Not found')], Http::STATUS_NOT_FOUND);
341
-		} catch (LockedException) {
342
-			return new DataResponse(['message' => $this->l->t('Node is locked')], Http::STATUS_INTERNAL_SERVER_ERROR);
343
-		} catch (Exception) {
344
-			return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
345
-		}
346
-	}
347
-
348
-	/**
349
-	 * Upload a file so it can be referenced in a task result (ExApp route version)
350
-	 *
351
-	 * Use field 'file' for the file upload
352
-	 *
353
-	 * @param int $taskId The id of the task
354
-	 * @return DataResponse<Http::STATUS_CREATED, array{fileId: int}, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}>
355
-	 *
356
-	 * 201: File created
357
-	 * 400: File upload failed or no file was uploaded
358
-	 * 404: Task not found
359
-	 */
360
-	#[ExAppRequired]
361
-	#[ApiRoute(verb: 'POST', url: '/tasks_provider/{taskId}/file', root: '/taskprocessing')]
362
-	public function setFileContentsExApp(int $taskId): DataResponse {
363
-		try {
364
-			$task = $this->taskProcessingManager->getTask($taskId);
365
-			$file = $this->request->getUploadedFile('file');
366
-			if (!isset($file['tmp_name'])) {
367
-				return new DataResponse(['message' => $this->l->t('Bad request')], Http::STATUS_BAD_REQUEST);
368
-			}
369
-			$handle = fopen($file['tmp_name'], 'r');
370
-			if (!$handle) {
371
-				return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
372
-			}
373
-			$fileId = $this->setFileContentsInternal($handle);
374
-			return new DataResponse(['fileId' => $fileId], Http::STATUS_CREATED);
375
-		} catch (NotFoundException) {
376
-			return new DataResponse(['message' => $this->l->t('Not found')], Http::STATUS_NOT_FOUND);
377
-		} catch (Exception) {
378
-			return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
379
-		}
380
-	}
381
-
382
-	/**
383
-	 * @throws NotPermittedException
384
-	 * @throws NotFoundException
385
-	 * @throws LockedException
386
-	 *
387
-	 * @return StreamResponse<Http::STATUS_OK, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}>
388
-	 */
389
-	private function getFileContentsInternal(Task $task, int $fileId): StreamResponse|DataResponse {
390
-		$ids = $this->taskProcessingManager->extractFileIdsFromTask($task);
391
-		if (!in_array($fileId, $ids)) {
392
-			return new DataResponse(['message' => $this->l->t('Not found')], Http::STATUS_NOT_FOUND);
393
-		}
394
-		if ($task->getUserId() !== null) {
395
-			\OC_Util::setupFS($task->getUserId());
396
-		}
397
-		$node = $this->rootFolder->getFirstNodeById($fileId);
398
-		if ($node === null) {
399
-			$node = $this->rootFolder->getFirstNodeByIdInPath($fileId, '/' . $this->rootFolder->getAppDataDirectoryName() . '/');
400
-			if (!$node instanceof File) {
401
-				throw new NotFoundException('Node is not a file');
402
-			}
403
-		} elseif (!$node instanceof File) {
404
-			throw new NotFoundException('Node is not a file');
405
-		}
406
-
407
-		$contentType = $node->getMimeType();
408
-		if (function_exists('mime_content_type')) {
409
-			$mimeType = mime_content_type($node->fopen('rb'));
410
-			if ($mimeType !== false) {
411
-				$mimeType = $this->mimeTypeDetector->getSecureMimeType($mimeType);
412
-				if ($mimeType !== 'application/octet-stream') {
413
-					$contentType = $mimeType;
414
-				}
415
-			}
416
-		}
417
-
418
-		$response = new StreamResponse($node->fopen('rb'));
419
-		$response->addHeader(
420
-			'Content-Disposition',
421
-			'attachment; filename="' . rawurldecode($node->getName()) . '"'
422
-		);
423
-		$response->addHeader('Content-Type', $contentType);
424
-		return $response;
425
-	}
426
-
427
-	/**
428
-	 * Sets the task progress
429
-	 *
430
-	 * @param int $taskId The id of the task
431
-	 * @param float $progress The progress
432
-	 * @return DataResponse<Http::STATUS_OK, array{task: CoreTaskProcessingTask}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}>
433
-	 *
434
-	 * 200: Progress updated successfully
435
-	 * 404: Task not found
436
-	 */
437
-	#[ExAppRequired]
438
-	#[ApiRoute(verb: 'POST', url: '/tasks_provider/{taskId}/progress', root: '/taskprocessing')]
439
-	public function setProgress(int $taskId, float $progress): DataResponse {
440
-		try {
441
-			$this->taskProcessingManager->setTaskProgress($taskId, $progress);
442
-			$task = $this->taskProcessingManager->getTask($taskId);
443
-
444
-			/** @var CoreTaskProcessingTask $json */
445
-			$json = $task->jsonSerialize();
446
-
447
-			return new DataResponse([
448
-				'task' => $json,
449
-			]);
450
-		} catch (NotFoundException) {
451
-			return new DataResponse(['message' => $this->l->t('Not found')], Http::STATUS_NOT_FOUND);
452
-		} catch (Exception) {
453
-			return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
454
-		}
455
-	}
456
-
457
-	/**
458
-	 * Sets the task result
459
-	 *
460
-	 * @param int $taskId The id of the task
461
-	 * @param array<string,mixed>|null $output The resulting task output, files are represented by their IDs
462
-	 * @param string|null $errorMessage An error message if the task failed
463
-	 * @param string|null $userFacingErrorMessage An error message that will be shown to the user
464
-	 * @return DataResponse<Http::STATUS_OK, array{task: CoreTaskProcessingTask}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}>
465
-	 *
466
-	 * 200: Result updated successfully
467
-	 * 404: Task not found
468
-	 */
469
-	#[ExAppRequired]
470
-	#[ApiRoute(verb: 'POST', url: '/tasks_provider/{taskId}/result', root: '/taskprocessing')]
471
-	public function setResult(int $taskId, ?array $output = null, ?string $errorMessage = null, ?string $userFacingErrorMessage = null): DataResponse {
472
-		try {
473
-			// set result
474
-			$this->taskProcessingManager->setTaskResult($taskId, $errorMessage, $output, isUsingFileIds: true, userFacingError: $userFacingErrorMessage);
475
-			$task = $this->taskProcessingManager->getTask($taskId);
476
-
477
-			/** @var CoreTaskProcessingTask $json */
478
-			$json = $task->jsonSerialize();
479
-
480
-			return new DataResponse([
481
-				'task' => $json,
482
-			]);
483
-		} catch (NotFoundException) {
484
-			return new DataResponse(['message' => $this->l->t('Not found')], Http::STATUS_NOT_FOUND);
485
-		} catch (Exception) {
486
-			return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
487
-		}
488
-	}
489
-
490
-	/**
491
-	 * Cancels a task
492
-	 *
493
-	 * @param int $taskId The id of the task
494
-	 * @return DataResponse<Http::STATUS_OK, array{task: CoreTaskProcessingTask}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}>
495
-	 *
496
-	 * 200: Task canceled successfully
497
-	 * 404: Task not found
498
-	 */
499
-	#[NoAdminRequired]
500
-	#[ApiRoute(verb: 'POST', url: '/tasks/{taskId}/cancel', root: '/taskprocessing')]
501
-	public function cancelTask(int $taskId): DataResponse {
502
-		try {
503
-			// Check if the current user can access the task
504
-			$this->taskProcessingManager->getUserTask($taskId, $this->userId);
505
-			// set result
506
-			$this->taskProcessingManager->cancelTask($taskId);
507
-			$task = $this->taskProcessingManager->getUserTask($taskId, $this->userId);
508
-
509
-			/** @var CoreTaskProcessingTask $json */
510
-			$json = $task->jsonSerialize();
511
-
512
-			return new DataResponse([
513
-				'task' => $json,
514
-			]);
515
-		} catch (NotFoundException) {
516
-			return new DataResponse(['message' => $this->l->t('Not found')], Http::STATUS_NOT_FOUND);
517
-		} catch (Exception) {
518
-			return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
519
-		}
520
-	}
521
-
522
-	/**
523
-	 * Returns the next scheduled task for the taskTypeId
524
-	 *
525
-	 * @param list<string> $providerIds The ids of the providers
526
-	 * @param list<string> $taskTypeIds The ids of the task types
527
-	 * @return DataResponse<Http::STATUS_OK, array{task: CoreTaskProcessingTask, provider: array{name: string}}, array{}>|DataResponse<Http::STATUS_NO_CONTENT, null, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
528
-	 *
529
-	 * 200: Task returned
530
-	 * 204: No task found
531
-	 */
532
-	#[ExAppRequired]
533
-	#[ApiRoute(verb: 'GET', url: '/tasks_provider/next', root: '/taskprocessing')]
534
-	public function getNextScheduledTask(array $providerIds, array $taskTypeIds): DataResponse {
535
-		try {
536
-			[$possibleProviderIds, $possibleTaskTypeIds] = $this->intersectTaskTypesAndProviders($taskTypeIds, $providerIds);
537
-
538
-			if (count($possibleProviderIds) === 0 || count($possibleTaskTypeIds) === 0) {
539
-				throw new NotFoundException();
540
-			}
541
-
542
-			$taskIdsToIgnore = [];
543
-			while (true) {
544
-				// Until we find a task whose task type is set to be provided by the providers requested with this request
545
-				// Or no scheduled task is found anymore (given the taskIds to ignore)
546
-				$task = $this->taskProcessingManager->getNextScheduledTask($possibleTaskTypeIds, $taskIdsToIgnore);
547
-				try {
548
-					$provider = $this->taskProcessingManager->getPreferredProvider($task->getTaskTypeId());
549
-					if (in_array($provider->getId(), $possibleProviderIds, true)) {
550
-						if ($this->taskProcessingManager->lockTask($task)) {
551
-							break;
552
-						}
553
-					}
554
-				} catch (Exception) {
555
-					// There is no provider set for the task type of this task
556
-					// proceed to ignore this task
557
-				}
558
-
559
-				$taskIdsToIgnore[] = (int)$task->getId();
560
-			}
561
-
562
-			/** @var CoreTaskProcessingTask $json */
563
-			$json = $task->jsonSerialize();
564
-
565
-			return new DataResponse([
566
-				'task' => $json,
567
-				'provider' => [
568
-					'name' => $provider->getId(),
569
-				],
570
-			]);
571
-		} catch (NotFoundException) {
572
-			return new DataResponse(null, Http::STATUS_NO_CONTENT);
573
-		} catch (Exception) {
574
-			return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
575
-		}
576
-	}
577
-
578
-	/**
579
-	 * Returns the next n scheduled tasks for the specified set of taskTypes and providers
580
-	 * The returned tasks are capped at ~50MiB
581
-	 *
582
-	 * @param list<string> $providerIds The ids of the providers
583
-	 * @param list<string> $taskTypeIds The ids of the task types
584
-	 * @param int $numberOfTasks The number of tasks to return
585
-	 * @return DataResponse<Http::STATUS_OK, array{tasks: list<array{task: CoreTaskProcessingTask, provider: string}>, has_more: bool}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
586
-	 *
587
-	 * 200: Tasks returned
588
-	 */
589
-	#[ExAppRequired]
590
-	#[ApiRoute(verb: 'GET', url: '/tasks_provider/next_batch', root: '/taskprocessing')]
591
-	public function getNextScheduledTaskBatch(array $providerIds, array $taskTypeIds, int $numberOfTasks = 1): DataResponse {
592
-		try {
593
-			[$possibleProviderIds, $possibleTaskTypeIds] = $this->intersectTaskTypesAndProviders($taskTypeIds, $providerIds);
594
-
595
-			if (count($possibleProviderIds) === 0 || count($possibleTaskTypeIds) === 0) {
596
-				return new DataResponse([
597
-					'tasks' => [],
598
-					'has_more' => false,
599
-				]);
600
-			}
601
-
602
-			$tasks = $this->taskProcessingManager->getNextScheduledTasks($possibleTaskTypeIds, numberOfTasks: $numberOfTasks + 1);
603
-			$tasksJson = [];
604
-			// Stop when $numberOfTasks is reached or the json payload is larger than 50MiB
605
-			while (count($tasks) > 0 && count($tasksJson) < $numberOfTasks && strlen(json_encode($tasks)) < 50 * 1024 * 1024) {
606
-				// Until we find a task whose task type is set to be provided by the providers requested with this request
607
-				// Or no scheduled task is found anymore (given the taskIds to ignore)
608
-				$task = array_shift($tasks);
609
-				try {
610
-					$provider = $this->taskProcessingManager->getPreferredProvider($task->getTaskTypeId());
611
-					if (in_array($provider->getId(), $possibleProviderIds, true)) {
612
-						if ($this->taskProcessingManager->lockTask($task)) {
613
-							$tasksJson[] = ['task' => $task->jsonSerialize(), 'provider' => $provider->getId()];
614
-							continue;
615
-						}
616
-					}
617
-				} catch (Exception) {
618
-					// There is no provider set for the task type of this task
619
-					// proceed to ignore this task
620
-				}
621
-			}
622
-			$hasMore = count($tasks) > 0;
623
-
624
-			return new DataResponse([
625
-				'tasks' => $tasksJson,
626
-				'has_more' => $hasMore,
627
-			]);
628
-		} catch (Exception) {
629
-			return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
630
-		}
631
-	}
632
-
633
-	/**
634
-	 * @param resource $data
635
-	 * @return int
636
-	 * @throws NotPermittedException
637
-	 */
638
-	private function setFileContentsInternal($data): int {
639
-		try {
640
-			$folder = $this->appData->getFolder('TaskProcessing');
641
-		} catch (\OCP\Files\NotFoundException) {
642
-			$folder = $this->appData->newFolder('TaskProcessing');
643
-		}
644
-		/** @var SimpleFile $file */
645
-		$file = $folder->newFile(time() . '-' . rand(1, 100000), $data);
646
-		return $file->getId();
647
-	}
648
-
649
-	/**
650
-	 * @param array $taskTypeIds
651
-	 * @param array $providerIds
652
-	 * @return array
653
-	 */
654
-	private function intersectTaskTypesAndProviders(array $taskTypeIds, array $providerIds): array {
655
-		$providerIdsBasedOnTaskTypesWithNull = array_unique(array_map(function ($taskTypeId) {
656
-			try {
657
-				return $this->taskProcessingManager->getPreferredProvider($taskTypeId)->getId();
658
-			} catch (Exception) {
659
-				return null;
660
-			}
661
-		}, $taskTypeIds));
662
-
663
-		$providerIdsBasedOnTaskTypes = array_filter($providerIdsBasedOnTaskTypesWithNull, fn ($providerId) => $providerId !== null);
664
-
665
-		// restrict $providerIds to providers that are configured as preferred for the passed task types
666
-		$possibleProviderIds = array_values(array_intersect($providerIdsBasedOnTaskTypes, $providerIds));
667
-
668
-		// restrict $taskTypeIds to task types that can actually be run by one of the now restricted providers
669
-		$possibleTaskTypeIds = array_values(array_filter($taskTypeIds, function ($taskTypeId) use ($possibleProviderIds) {
670
-			try {
671
-				$providerForTaskType = $this->taskProcessingManager->getPreferredProvider($taskTypeId)->getId();
672
-			} catch (Exception) {
673
-				// no provider found for task type
674
-				return false;
675
-			}
676
-			return in_array($providerForTaskType, $possibleProviderIds, true);
677
-		}));
678
-		return [$possibleProviderIds, $possibleTaskTypeIds];
679
-	}
48
+    public function __construct(
49
+        string $appName,
50
+        IRequest $request,
51
+        private IManager $taskProcessingManager,
52
+        private IL10N $l,
53
+        private ?string $userId,
54
+        private IRootFolder $rootFolder,
55
+        private IAppData $appData,
56
+        private IMimeTypeDetector $mimeTypeDetector,
57
+    ) {
58
+        parent::__construct($appName, $request);
59
+    }
60
+
61
+    /**
62
+     * Returns all available TaskProcessing task types
63
+     *
64
+     * @return DataResponse<Http::STATUS_OK, array{types: array<string, CoreTaskProcessingTaskType>}, array{}>
65
+     *
66
+     * 200: Task types returned
67
+     */
68
+    #[NoAdminRequired]
69
+    #[ApiRoute(verb: 'GET', url: '/tasktypes', root: '/taskprocessing')]
70
+    public function taskTypes(): DataResponse {
71
+        /** @var array<string, CoreTaskProcessingTaskType> $taskTypes */
72
+        $taskTypes = array_map(function (array $tt) {
73
+            $tt['inputShape'] = array_map(function ($descriptor) {
74
+                return $descriptor->jsonSerialize();
75
+            }, $tt['inputShape']);
76
+            if (empty($tt['inputShape'])) {
77
+                $tt['inputShape'] = new stdClass;
78
+            }
79
+
80
+            $tt['outputShape'] = array_map(function ($descriptor) {
81
+                return $descriptor->jsonSerialize();
82
+            }, $tt['outputShape']);
83
+            if (empty($tt['outputShape'])) {
84
+                $tt['outputShape'] = new stdClass;
85
+            }
86
+
87
+            $tt['optionalInputShape'] = array_map(function ($descriptor) {
88
+                return $descriptor->jsonSerialize();
89
+            }, $tt['optionalInputShape']);
90
+            if (empty($tt['optionalInputShape'])) {
91
+                $tt['optionalInputShape'] = new stdClass;
92
+            }
93
+
94
+            $tt['optionalOutputShape'] = array_map(function ($descriptor) {
95
+                return $descriptor->jsonSerialize();
96
+            }, $tt['optionalOutputShape']);
97
+            if (empty($tt['optionalOutputShape'])) {
98
+                $tt['optionalOutputShape'] = new stdClass;
99
+            }
100
+
101
+            $tt['inputShapeEnumValues'] = array_map(function (array $enumValues) {
102
+                return array_map(fn (ShapeEnumValue $enumValue) => $enumValue->jsonSerialize(), $enumValues);
103
+            }, $tt['inputShapeEnumValues']);
104
+            if (empty($tt['inputShapeEnumValues'])) {
105
+                $tt['inputShapeEnumValues'] = new stdClass;
106
+            }
107
+
108
+            $tt['optionalInputShapeEnumValues'] = array_map(function (array $enumValues) {
109
+                return array_map(fn (ShapeEnumValue $enumValue) => $enumValue->jsonSerialize(), $enumValues);
110
+            }, $tt['optionalInputShapeEnumValues']);
111
+            if (empty($tt['optionalInputShapeEnumValues'])) {
112
+                $tt['optionalInputShapeEnumValues'] = new stdClass;
113
+            }
114
+
115
+            $tt['outputShapeEnumValues'] = array_map(function (array $enumValues) {
116
+                return array_map(fn (ShapeEnumValue $enumValue) => $enumValue->jsonSerialize(), $enumValues);
117
+            }, $tt['outputShapeEnumValues']);
118
+            if (empty($tt['outputShapeEnumValues'])) {
119
+                $tt['outputShapeEnumValues'] = new stdClass;
120
+            }
121
+
122
+            $tt['optionalOutputShapeEnumValues'] = array_map(function (array $enumValues) {
123
+                return array_map(fn (ShapeEnumValue $enumValue) => $enumValue->jsonSerialize(), $enumValues);
124
+            }, $tt['optionalOutputShapeEnumValues']);
125
+            if (empty($tt['optionalOutputShapeEnumValues'])) {
126
+                $tt['optionalOutputShapeEnumValues'] = new stdClass;
127
+            }
128
+
129
+            if (empty($tt['inputShapeDefaults'])) {
130
+                $tt['inputShapeDefaults'] = new stdClass;
131
+            }
132
+            if (empty($tt['optionalInputShapeDefaults'])) {
133
+                $tt['optionalInputShapeDefaults'] = new stdClass;
134
+            }
135
+            return $tt;
136
+        }, $this->taskProcessingManager->getAvailableTaskTypes());
137
+        return new DataResponse([
138
+            'types' => $taskTypes,
139
+        ]);
140
+    }
141
+
142
+    /**
143
+     * Schedules a task
144
+     *
145
+     * @param array<string, mixed> $input Task's input parameters
146
+     * @param string $type Type of the task
147
+     * @param string $appId ID of the app that will execute the task
148
+     * @param string $customId An arbitrary identifier for the task
149
+     * @param string|null $webhookUri URI to be requested when the task finishes
150
+     * @param string|null $webhookMethod Method used for the webhook request (HTTP:GET, HTTP:POST, HTTP:PUT, HTTP:DELETE or AppAPI:APP_ID:GET, AppAPI:APP_ID:POST...)
151
+     * @return DataResponse<Http::STATUS_OK, array{task: CoreTaskProcessingTask}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_BAD_REQUEST|Http::STATUS_PRECONDITION_FAILED|Http::STATUS_UNAUTHORIZED, array{message: string}, array{}>
152
+     *
153
+     * 200: Task scheduled successfully
154
+     * 400: Scheduling task is not possible
155
+     * 412: Scheduling task is not possible
156
+     * 401: Cannot schedule task because it references files in its input that the user doesn't have access to
157
+     */
158
+    #[UserRateLimit(limit: 20, period: 120)]
159
+    #[NoAdminRequired]
160
+    #[ApiRoute(verb: 'POST', url: '/schedule', root: '/taskprocessing')]
161
+    public function schedule(
162
+        array $input, string $type, string $appId, string $customId = '',
163
+        ?string $webhookUri = null, ?string $webhookMethod = null,
164
+    ): DataResponse {
165
+        $task = new Task($type, $input, $appId, $this->userId, $customId);
166
+        $task->setWebhookUri($webhookUri);
167
+        $task->setWebhookMethod($webhookMethod);
168
+        try {
169
+            $this->taskProcessingManager->scheduleTask($task);
170
+
171
+            /** @var CoreTaskProcessingTask $json */
172
+            $json = $task->jsonSerialize();
173
+
174
+            return new DataResponse([
175
+                'task' => $json,
176
+            ]);
177
+        } catch (PreConditionNotMetException) {
178
+            return new DataResponse(['message' => $this->l->t('The given provider is not available')], Http::STATUS_PRECONDITION_FAILED);
179
+        } catch (ValidationException $e) {
180
+            return new DataResponse(['message' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
181
+        } catch (UnauthorizedException) {
182
+            return new DataResponse(['message' => 'User does not have access to the files mentioned in the task input'], Http::STATUS_UNAUTHORIZED);
183
+        } catch (Exception) {
184
+            return new DataResponse(['message' => 'Internal server error'], Http::STATUS_INTERNAL_SERVER_ERROR);
185
+        }
186
+    }
187
+
188
+    /**
189
+     * Gets a task including status and result
190
+     *
191
+     * Tasks are removed 1 week after receiving their last update
192
+     *
193
+     * @param int $id The id of the task
194
+     *
195
+     * @return DataResponse<Http::STATUS_OK, array{task: CoreTaskProcessingTask}, array{}>|DataResponse<Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
196
+     *
197
+     * 200: Task returned
198
+     * 404: Task not found
199
+     */
200
+    #[NoAdminRequired]
201
+    #[ApiRoute(verb: 'GET', url: '/task/{id}', root: '/taskprocessing')]
202
+    public function getTask(int $id): DataResponse {
203
+        try {
204
+            $task = $this->taskProcessingManager->getUserTask($id, $this->userId);
205
+
206
+            /** @var CoreTaskProcessingTask $json */
207
+            $json = $task->jsonSerialize();
208
+
209
+            return new DataResponse([
210
+                'task' => $json,
211
+            ]);
212
+        } catch (NotFoundException) {
213
+            return new DataResponse(['message' => $this->l->t('Task not found')], Http::STATUS_NOT_FOUND);
214
+        } catch (RuntimeException) {
215
+            return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
216
+        }
217
+    }
218
+
219
+    /**
220
+     * Deletes a task
221
+     *
222
+     * @param int $id The id of the task
223
+     *
224
+     * @return DataResponse<Http::STATUS_OK, null, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
225
+     *
226
+     * 200: Task deleted
227
+     */
228
+    #[NoAdminRequired]
229
+    #[ApiRoute(verb: 'DELETE', url: '/task/{id}', root: '/taskprocessing')]
230
+    public function deleteTask(int $id): DataResponse {
231
+        try {
232
+            $task = $this->taskProcessingManager->getUserTask($id, $this->userId);
233
+
234
+            $this->taskProcessingManager->deleteTask($task);
235
+
236
+            return new DataResponse(null);
237
+        } catch (NotFoundException) {
238
+            return new DataResponse(null);
239
+        } catch (Exception) {
240
+            return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
241
+        }
242
+    }
243
+
244
+
245
+    /**
246
+     * Returns tasks for the current user filtered by the appId and optional customId
247
+     *
248
+     * @param string $appId ID of the app
249
+     * @param string|null $customId An arbitrary identifier for the task
250
+     * @return DataResponse<Http::STATUS_OK, array{tasks: list<CoreTaskProcessingTask>}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
251
+     *
252
+     * 200: Tasks returned
253
+     */
254
+    #[NoAdminRequired]
255
+    #[ApiRoute(verb: 'GET', url: '/tasks/app/{appId}', root: '/taskprocessing')]
256
+    public function listTasksByApp(string $appId, ?string $customId = null): DataResponse {
257
+        try {
258
+            $tasks = $this->taskProcessingManager->getUserTasksByApp($this->userId, $appId, $customId);
259
+            $json = array_map(static function (Task $task) {
260
+                return $task->jsonSerialize();
261
+            }, $tasks);
262
+
263
+            return new DataResponse([
264
+                'tasks' => $json,
265
+            ]);
266
+        } catch (Exception) {
267
+            return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
268
+        }
269
+    }
270
+
271
+    /**
272
+     * Returns tasks for the current user filtered by the optional taskType and optional customId
273
+     *
274
+     * @param string|null $taskType The task type to filter by
275
+     * @param string|null $customId An arbitrary identifier for the task
276
+     * @return DataResponse<Http::STATUS_OK, array{tasks: list<CoreTaskProcessingTask>}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
277
+     *
278
+     * 200: Tasks returned
279
+     */
280
+    #[NoAdminRequired]
281
+    #[ApiRoute(verb: 'GET', url: '/tasks', root: '/taskprocessing')]
282
+    public function listTasks(?string $taskType, ?string $customId = null): DataResponse {
283
+        try {
284
+            $tasks = $this->taskProcessingManager->getUserTasks($this->userId, $taskType, $customId);
285
+            $json = array_map(static function (Task $task) {
286
+                return $task->jsonSerialize();
287
+            }, $tasks);
288
+
289
+            return new DataResponse([
290
+                'tasks' => $json,
291
+            ]);
292
+        } catch (Exception) {
293
+            return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
294
+        }
295
+    }
296
+
297
+    /**
298
+     * Returns the contents of a file referenced in a task
299
+     *
300
+     * @param int $taskId The id of the task
301
+     * @param int $fileId The file id of the file to retrieve
302
+     * @return StreamResponse<Http::STATUS_OK, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}>
303
+     *
304
+     * 200: File content returned
305
+     * 404: Task or file not found
306
+     */
307
+    #[NoAdminRequired]
308
+    #[NoCSRFRequired]
309
+    #[ApiRoute(verb: 'GET', url: '/tasks/{taskId}/file/{fileId}', root: '/taskprocessing')]
310
+    public function getFileContents(int $taskId, int $fileId): StreamResponse|DataResponse {
311
+        try {
312
+            $task = $this->taskProcessingManager->getUserTask($taskId, $this->userId);
313
+            return $this->getFileContentsInternal($task, $fileId);
314
+        } catch (NotFoundException) {
315
+            return new DataResponse(['message' => $this->l->t('Not found')], Http::STATUS_NOT_FOUND);
316
+        } catch (LockedException) {
317
+            return new DataResponse(['message' => $this->l->t('Node is locked')], Http::STATUS_INTERNAL_SERVER_ERROR);
318
+        } catch (Exception) {
319
+            return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
320
+        }
321
+    }
322
+
323
+    /**
324
+     * Returns the contents of a file referenced in a task(ExApp route version)
325
+     *
326
+     * @param int $taskId The id of the task
327
+     * @param int $fileId The file id of the file to retrieve
328
+     * @return StreamResponse<Http::STATUS_OK, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}>
329
+     *
330
+     * 200: File content returned
331
+     * 404: Task or file not found
332
+     */
333
+    #[ExAppRequired]
334
+    #[ApiRoute(verb: 'GET', url: '/tasks_provider/{taskId}/file/{fileId}', root: '/taskprocessing')]
335
+    public function getFileContentsExApp(int $taskId, int $fileId): StreamResponse|DataResponse {
336
+        try {
337
+            $task = $this->taskProcessingManager->getTask($taskId);
338
+            return $this->getFileContentsInternal($task, $fileId);
339
+        } catch (NotFoundException) {
340
+            return new DataResponse(['message' => $this->l->t('Not found')], Http::STATUS_NOT_FOUND);
341
+        } catch (LockedException) {
342
+            return new DataResponse(['message' => $this->l->t('Node is locked')], Http::STATUS_INTERNAL_SERVER_ERROR);
343
+        } catch (Exception) {
344
+            return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
345
+        }
346
+    }
347
+
348
+    /**
349
+     * Upload a file so it can be referenced in a task result (ExApp route version)
350
+     *
351
+     * Use field 'file' for the file upload
352
+     *
353
+     * @param int $taskId The id of the task
354
+     * @return DataResponse<Http::STATUS_CREATED, array{fileId: int}, array{}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}>
355
+     *
356
+     * 201: File created
357
+     * 400: File upload failed or no file was uploaded
358
+     * 404: Task not found
359
+     */
360
+    #[ExAppRequired]
361
+    #[ApiRoute(verb: 'POST', url: '/tasks_provider/{taskId}/file', root: '/taskprocessing')]
362
+    public function setFileContentsExApp(int $taskId): DataResponse {
363
+        try {
364
+            $task = $this->taskProcessingManager->getTask($taskId);
365
+            $file = $this->request->getUploadedFile('file');
366
+            if (!isset($file['tmp_name'])) {
367
+                return new DataResponse(['message' => $this->l->t('Bad request')], Http::STATUS_BAD_REQUEST);
368
+            }
369
+            $handle = fopen($file['tmp_name'], 'r');
370
+            if (!$handle) {
371
+                return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
372
+            }
373
+            $fileId = $this->setFileContentsInternal($handle);
374
+            return new DataResponse(['fileId' => $fileId], Http::STATUS_CREATED);
375
+        } catch (NotFoundException) {
376
+            return new DataResponse(['message' => $this->l->t('Not found')], Http::STATUS_NOT_FOUND);
377
+        } catch (Exception) {
378
+            return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
379
+        }
380
+    }
381
+
382
+    /**
383
+     * @throws NotPermittedException
384
+     * @throws NotFoundException
385
+     * @throws LockedException
386
+     *
387
+     * @return StreamResponse<Http::STATUS_OK, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}>
388
+     */
389
+    private function getFileContentsInternal(Task $task, int $fileId): StreamResponse|DataResponse {
390
+        $ids = $this->taskProcessingManager->extractFileIdsFromTask($task);
391
+        if (!in_array($fileId, $ids)) {
392
+            return new DataResponse(['message' => $this->l->t('Not found')], Http::STATUS_NOT_FOUND);
393
+        }
394
+        if ($task->getUserId() !== null) {
395
+            \OC_Util::setupFS($task->getUserId());
396
+        }
397
+        $node = $this->rootFolder->getFirstNodeById($fileId);
398
+        if ($node === null) {
399
+            $node = $this->rootFolder->getFirstNodeByIdInPath($fileId, '/' . $this->rootFolder->getAppDataDirectoryName() . '/');
400
+            if (!$node instanceof File) {
401
+                throw new NotFoundException('Node is not a file');
402
+            }
403
+        } elseif (!$node instanceof File) {
404
+            throw new NotFoundException('Node is not a file');
405
+        }
406
+
407
+        $contentType = $node->getMimeType();
408
+        if (function_exists('mime_content_type')) {
409
+            $mimeType = mime_content_type($node->fopen('rb'));
410
+            if ($mimeType !== false) {
411
+                $mimeType = $this->mimeTypeDetector->getSecureMimeType($mimeType);
412
+                if ($mimeType !== 'application/octet-stream') {
413
+                    $contentType = $mimeType;
414
+                }
415
+            }
416
+        }
417
+
418
+        $response = new StreamResponse($node->fopen('rb'));
419
+        $response->addHeader(
420
+            'Content-Disposition',
421
+            'attachment; filename="' . rawurldecode($node->getName()) . '"'
422
+        );
423
+        $response->addHeader('Content-Type', $contentType);
424
+        return $response;
425
+    }
426
+
427
+    /**
428
+     * Sets the task progress
429
+     *
430
+     * @param int $taskId The id of the task
431
+     * @param float $progress The progress
432
+     * @return DataResponse<Http::STATUS_OK, array{task: CoreTaskProcessingTask}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}>
433
+     *
434
+     * 200: Progress updated successfully
435
+     * 404: Task not found
436
+     */
437
+    #[ExAppRequired]
438
+    #[ApiRoute(verb: 'POST', url: '/tasks_provider/{taskId}/progress', root: '/taskprocessing')]
439
+    public function setProgress(int $taskId, float $progress): DataResponse {
440
+        try {
441
+            $this->taskProcessingManager->setTaskProgress($taskId, $progress);
442
+            $task = $this->taskProcessingManager->getTask($taskId);
443
+
444
+            /** @var CoreTaskProcessingTask $json */
445
+            $json = $task->jsonSerialize();
446
+
447
+            return new DataResponse([
448
+                'task' => $json,
449
+            ]);
450
+        } catch (NotFoundException) {
451
+            return new DataResponse(['message' => $this->l->t('Not found')], Http::STATUS_NOT_FOUND);
452
+        } catch (Exception) {
453
+            return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
454
+        }
455
+    }
456
+
457
+    /**
458
+     * Sets the task result
459
+     *
460
+     * @param int $taskId The id of the task
461
+     * @param array<string,mixed>|null $output The resulting task output, files are represented by their IDs
462
+     * @param string|null $errorMessage An error message if the task failed
463
+     * @param string|null $userFacingErrorMessage An error message that will be shown to the user
464
+     * @return DataResponse<Http::STATUS_OK, array{task: CoreTaskProcessingTask}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}>
465
+     *
466
+     * 200: Result updated successfully
467
+     * 404: Task not found
468
+     */
469
+    #[ExAppRequired]
470
+    #[ApiRoute(verb: 'POST', url: '/tasks_provider/{taskId}/result', root: '/taskprocessing')]
471
+    public function setResult(int $taskId, ?array $output = null, ?string $errorMessage = null, ?string $userFacingErrorMessage = null): DataResponse {
472
+        try {
473
+            // set result
474
+            $this->taskProcessingManager->setTaskResult($taskId, $errorMessage, $output, isUsingFileIds: true, userFacingError: $userFacingErrorMessage);
475
+            $task = $this->taskProcessingManager->getTask($taskId);
476
+
477
+            /** @var CoreTaskProcessingTask $json */
478
+            $json = $task->jsonSerialize();
479
+
480
+            return new DataResponse([
481
+                'task' => $json,
482
+            ]);
483
+        } catch (NotFoundException) {
484
+            return new DataResponse(['message' => $this->l->t('Not found')], Http::STATUS_NOT_FOUND);
485
+        } catch (Exception) {
486
+            return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
487
+        }
488
+    }
489
+
490
+    /**
491
+     * Cancels a task
492
+     *
493
+     * @param int $taskId The id of the task
494
+     * @return DataResponse<Http::STATUS_OK, array{task: CoreTaskProcessingTask}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_NOT_FOUND, array{message: string}, array{}>
495
+     *
496
+     * 200: Task canceled successfully
497
+     * 404: Task not found
498
+     */
499
+    #[NoAdminRequired]
500
+    #[ApiRoute(verb: 'POST', url: '/tasks/{taskId}/cancel', root: '/taskprocessing')]
501
+    public function cancelTask(int $taskId): DataResponse {
502
+        try {
503
+            // Check if the current user can access the task
504
+            $this->taskProcessingManager->getUserTask($taskId, $this->userId);
505
+            // set result
506
+            $this->taskProcessingManager->cancelTask($taskId);
507
+            $task = $this->taskProcessingManager->getUserTask($taskId, $this->userId);
508
+
509
+            /** @var CoreTaskProcessingTask $json */
510
+            $json = $task->jsonSerialize();
511
+
512
+            return new DataResponse([
513
+                'task' => $json,
514
+            ]);
515
+        } catch (NotFoundException) {
516
+            return new DataResponse(['message' => $this->l->t('Not found')], Http::STATUS_NOT_FOUND);
517
+        } catch (Exception) {
518
+            return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
519
+        }
520
+    }
521
+
522
+    /**
523
+     * Returns the next scheduled task for the taskTypeId
524
+     *
525
+     * @param list<string> $providerIds The ids of the providers
526
+     * @param list<string> $taskTypeIds The ids of the task types
527
+     * @return DataResponse<Http::STATUS_OK, array{task: CoreTaskProcessingTask, provider: array{name: string}}, array{}>|DataResponse<Http::STATUS_NO_CONTENT, null, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
528
+     *
529
+     * 200: Task returned
530
+     * 204: No task found
531
+     */
532
+    #[ExAppRequired]
533
+    #[ApiRoute(verb: 'GET', url: '/tasks_provider/next', root: '/taskprocessing')]
534
+    public function getNextScheduledTask(array $providerIds, array $taskTypeIds): DataResponse {
535
+        try {
536
+            [$possibleProviderIds, $possibleTaskTypeIds] = $this->intersectTaskTypesAndProviders($taskTypeIds, $providerIds);
537
+
538
+            if (count($possibleProviderIds) === 0 || count($possibleTaskTypeIds) === 0) {
539
+                throw new NotFoundException();
540
+            }
541
+
542
+            $taskIdsToIgnore = [];
543
+            while (true) {
544
+                // Until we find a task whose task type is set to be provided by the providers requested with this request
545
+                // Or no scheduled task is found anymore (given the taskIds to ignore)
546
+                $task = $this->taskProcessingManager->getNextScheduledTask($possibleTaskTypeIds, $taskIdsToIgnore);
547
+                try {
548
+                    $provider = $this->taskProcessingManager->getPreferredProvider($task->getTaskTypeId());
549
+                    if (in_array($provider->getId(), $possibleProviderIds, true)) {
550
+                        if ($this->taskProcessingManager->lockTask($task)) {
551
+                            break;
552
+                        }
553
+                    }
554
+                } catch (Exception) {
555
+                    // There is no provider set for the task type of this task
556
+                    // proceed to ignore this task
557
+                }
558
+
559
+                $taskIdsToIgnore[] = (int)$task->getId();
560
+            }
561
+
562
+            /** @var CoreTaskProcessingTask $json */
563
+            $json = $task->jsonSerialize();
564
+
565
+            return new DataResponse([
566
+                'task' => $json,
567
+                'provider' => [
568
+                    'name' => $provider->getId(),
569
+                ],
570
+            ]);
571
+        } catch (NotFoundException) {
572
+            return new DataResponse(null, Http::STATUS_NO_CONTENT);
573
+        } catch (Exception) {
574
+            return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
575
+        }
576
+    }
577
+
578
+    /**
579
+     * Returns the next n scheduled tasks for the specified set of taskTypes and providers
580
+     * The returned tasks are capped at ~50MiB
581
+     *
582
+     * @param list<string> $providerIds The ids of the providers
583
+     * @param list<string> $taskTypeIds The ids of the task types
584
+     * @param int $numberOfTasks The number of tasks to return
585
+     * @return DataResponse<Http::STATUS_OK, array{tasks: list<array{task: CoreTaskProcessingTask, provider: string}>, has_more: bool}, array{}>|DataResponse<Http::STATUS_INTERNAL_SERVER_ERROR, array{message: string}, array{}>
586
+     *
587
+     * 200: Tasks returned
588
+     */
589
+    #[ExAppRequired]
590
+    #[ApiRoute(verb: 'GET', url: '/tasks_provider/next_batch', root: '/taskprocessing')]
591
+    public function getNextScheduledTaskBatch(array $providerIds, array $taskTypeIds, int $numberOfTasks = 1): DataResponse {
592
+        try {
593
+            [$possibleProviderIds, $possibleTaskTypeIds] = $this->intersectTaskTypesAndProviders($taskTypeIds, $providerIds);
594
+
595
+            if (count($possibleProviderIds) === 0 || count($possibleTaskTypeIds) === 0) {
596
+                return new DataResponse([
597
+                    'tasks' => [],
598
+                    'has_more' => false,
599
+                ]);
600
+            }
601
+
602
+            $tasks = $this->taskProcessingManager->getNextScheduledTasks($possibleTaskTypeIds, numberOfTasks: $numberOfTasks + 1);
603
+            $tasksJson = [];
604
+            // Stop when $numberOfTasks is reached or the json payload is larger than 50MiB
605
+            while (count($tasks) > 0 && count($tasksJson) < $numberOfTasks && strlen(json_encode($tasks)) < 50 * 1024 * 1024) {
606
+                // Until we find a task whose task type is set to be provided by the providers requested with this request
607
+                // Or no scheduled task is found anymore (given the taskIds to ignore)
608
+                $task = array_shift($tasks);
609
+                try {
610
+                    $provider = $this->taskProcessingManager->getPreferredProvider($task->getTaskTypeId());
611
+                    if (in_array($provider->getId(), $possibleProviderIds, true)) {
612
+                        if ($this->taskProcessingManager->lockTask($task)) {
613
+                            $tasksJson[] = ['task' => $task->jsonSerialize(), 'provider' => $provider->getId()];
614
+                            continue;
615
+                        }
616
+                    }
617
+                } catch (Exception) {
618
+                    // There is no provider set for the task type of this task
619
+                    // proceed to ignore this task
620
+                }
621
+            }
622
+            $hasMore = count($tasks) > 0;
623
+
624
+            return new DataResponse([
625
+                'tasks' => $tasksJson,
626
+                'has_more' => $hasMore,
627
+            ]);
628
+        } catch (Exception) {
629
+            return new DataResponse(['message' => $this->l->t('Internal error')], Http::STATUS_INTERNAL_SERVER_ERROR);
630
+        }
631
+    }
632
+
633
+    /**
634
+     * @param resource $data
635
+     * @return int
636
+     * @throws NotPermittedException
637
+     */
638
+    private function setFileContentsInternal($data): int {
639
+        try {
640
+            $folder = $this->appData->getFolder('TaskProcessing');
641
+        } catch (\OCP\Files\NotFoundException) {
642
+            $folder = $this->appData->newFolder('TaskProcessing');
643
+        }
644
+        /** @var SimpleFile $file */
645
+        $file = $folder->newFile(time() . '-' . rand(1, 100000), $data);
646
+        return $file->getId();
647
+    }
648
+
649
+    /**
650
+     * @param array $taskTypeIds
651
+     * @param array $providerIds
652
+     * @return array
653
+     */
654
+    private function intersectTaskTypesAndProviders(array $taskTypeIds, array $providerIds): array {
655
+        $providerIdsBasedOnTaskTypesWithNull = array_unique(array_map(function ($taskTypeId) {
656
+            try {
657
+                return $this->taskProcessingManager->getPreferredProvider($taskTypeId)->getId();
658
+            } catch (Exception) {
659
+                return null;
660
+            }
661
+        }, $taskTypeIds));
662
+
663
+        $providerIdsBasedOnTaskTypes = array_filter($providerIdsBasedOnTaskTypesWithNull, fn ($providerId) => $providerId !== null);
664
+
665
+        // restrict $providerIds to providers that are configured as preferred for the passed task types
666
+        $possibleProviderIds = array_values(array_intersect($providerIdsBasedOnTaskTypes, $providerIds));
667
+
668
+        // restrict $taskTypeIds to task types that can actually be run by one of the now restricted providers
669
+        $possibleTaskTypeIds = array_values(array_filter($taskTypeIds, function ($taskTypeId) use ($possibleProviderIds) {
670
+            try {
671
+                $providerForTaskType = $this->taskProcessingManager->getPreferredProvider($taskTypeId)->getId();
672
+            } catch (Exception) {
673
+                // no provider found for task type
674
+                return false;
675
+            }
676
+            return in_array($providerForTaskType, $possibleProviderIds, true);
677
+        }));
678
+        return [$possibleProviderIds, $possibleTaskTypeIds];
679
+    }
680 680
 }
Please login to merge, or discard this patch.
tests/lib/TaskProcessing/TaskProcessingTest.php 1 patch
Indentation   +1377 added lines, -1377 removed lines patch added patch discarded remove patch
@@ -60,1441 +60,1441 @@
 block discarded – undo
60 60
 use Test\BackgroundJob\DummyJobList;
61 61
 
62 62
 class AudioToImage implements ITaskType {
63
-	public const ID = 'test:audiotoimage';
64
-
65
-	public function getId(): string {
66
-		return self::ID;
67
-	}
68
-
69
-	public function getName(): string {
70
-		return self::class;
71
-	}
72
-
73
-	public function getDescription(): string {
74
-		return self::class;
75
-	}
76
-
77
-	public function getInputShape(): array {
78
-		return [
79
-			'audio' => new ShapeDescriptor('Audio', 'The audio', EShapeType::Audio),
80
-		];
81
-	}
82
-
83
-	public function getOutputShape(): array {
84
-		return [
85
-			'spectrogram' => new ShapeDescriptor('Spectrogram', 'The audio spectrogram', EShapeType::Image),
86
-		];
87
-	}
63
+    public const ID = 'test:audiotoimage';
64
+
65
+    public function getId(): string {
66
+        return self::ID;
67
+    }
68
+
69
+    public function getName(): string {
70
+        return self::class;
71
+    }
72
+
73
+    public function getDescription(): string {
74
+        return self::class;
75
+    }
76
+
77
+    public function getInputShape(): array {
78
+        return [
79
+            'audio' => new ShapeDescriptor('Audio', 'The audio', EShapeType::Audio),
80
+        ];
81
+    }
82
+
83
+    public function getOutputShape(): array {
84
+        return [
85
+            'spectrogram' => new ShapeDescriptor('Spectrogram', 'The audio spectrogram', EShapeType::Image),
86
+        ];
87
+    }
88 88
 }
89 89
 
90 90
 class AsyncProvider implements IProvider {
91
-	public function getId(): string {
92
-		return 'test:sync:success';
93
-	}
94
-
95
-	public function getName(): string {
96
-		return self::class;
97
-	}
98
-
99
-	public function getTaskTypeId(): string {
100
-		return AudioToImage::ID;
101
-	}
102
-
103
-	public function getExpectedRuntime(): int {
104
-		return 10;
105
-	}
106
-
107
-	public function getOptionalInputShape(): array {
108
-		return [
109
-			'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text),
110
-		];
111
-	}
112
-
113
-	public function getOptionalOutputShape(): array {
114
-		return [
115
-			'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text),
116
-		];
117
-	}
118
-
119
-	public function getInputShapeEnumValues(): array {
120
-		return [];
121
-	}
122
-
123
-	public function getInputShapeDefaults(): array {
124
-		return [];
125
-	}
126
-
127
-	public function getOptionalInputShapeEnumValues(): array {
128
-		return [];
129
-	}
130
-
131
-	public function getOptionalInputShapeDefaults(): array {
132
-		return [];
133
-	}
134
-
135
-	public function getOutputShapeEnumValues(): array {
136
-		return [];
137
-	}
138
-
139
-	public function getOptionalOutputShapeEnumValues(): array {
140
-		return [];
141
-	}
91
+    public function getId(): string {
92
+        return 'test:sync:success';
93
+    }
94
+
95
+    public function getName(): string {
96
+        return self::class;
97
+    }
98
+
99
+    public function getTaskTypeId(): string {
100
+        return AudioToImage::ID;
101
+    }
102
+
103
+    public function getExpectedRuntime(): int {
104
+        return 10;
105
+    }
106
+
107
+    public function getOptionalInputShape(): array {
108
+        return [
109
+            'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text),
110
+        ];
111
+    }
112
+
113
+    public function getOptionalOutputShape(): array {
114
+        return [
115
+            'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text),
116
+        ];
117
+    }
118
+
119
+    public function getInputShapeEnumValues(): array {
120
+        return [];
121
+    }
122
+
123
+    public function getInputShapeDefaults(): array {
124
+        return [];
125
+    }
126
+
127
+    public function getOptionalInputShapeEnumValues(): array {
128
+        return [];
129
+    }
130
+
131
+    public function getOptionalInputShapeDefaults(): array {
132
+        return [];
133
+    }
134
+
135
+    public function getOutputShapeEnumValues(): array {
136
+        return [];
137
+    }
138
+
139
+    public function getOptionalOutputShapeEnumValues(): array {
140
+        return [];
141
+    }
142 142
 }
143 143
 
144 144
 class SuccessfulSyncProvider implements IProvider, ISynchronousProvider {
145
-	public const ID = 'test:sync:success';
146
-
147
-	public function getId(): string {
148
-		return self::ID;
149
-	}
150
-
151
-	public function getName(): string {
152
-		return self::class;
153
-	}
154
-
155
-	public function getTaskTypeId(): string {
156
-		return TextToText::ID;
157
-	}
158
-
159
-	public function getExpectedRuntime(): int {
160
-		return 10;
161
-	}
162
-
163
-	public function getOptionalInputShape(): array {
164
-		return [
165
-			'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text),
166
-		];
167
-	}
168
-
169
-	public function getOptionalOutputShape(): array {
170
-		return [
171
-			'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text),
172
-		];
173
-	}
174
-
175
-	public function process(?string $userId, array $input, callable $reportProgress): array {
176
-		return ['output' => $input['input']];
177
-	}
178
-
179
-	public function getInputShapeEnumValues(): array {
180
-		return [];
181
-	}
182
-
183
-	public function getInputShapeDefaults(): array {
184
-		return [];
185
-	}
186
-
187
-	public function getOptionalInputShapeEnumValues(): array {
188
-		return [];
189
-	}
190
-
191
-	public function getOptionalInputShapeDefaults(): array {
192
-		return [];
193
-	}
194
-
195
-	public function getOutputShapeEnumValues(): array {
196
-		return [];
197
-	}
198
-
199
-	public function getOptionalOutputShapeEnumValues(): array {
200
-		return [];
201
-	}
145
+    public const ID = 'test:sync:success';
146
+
147
+    public function getId(): string {
148
+        return self::ID;
149
+    }
150
+
151
+    public function getName(): string {
152
+        return self::class;
153
+    }
154
+
155
+    public function getTaskTypeId(): string {
156
+        return TextToText::ID;
157
+    }
158
+
159
+    public function getExpectedRuntime(): int {
160
+        return 10;
161
+    }
162
+
163
+    public function getOptionalInputShape(): array {
164
+        return [
165
+            'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text),
166
+        ];
167
+    }
168
+
169
+    public function getOptionalOutputShape(): array {
170
+        return [
171
+            'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text),
172
+        ];
173
+    }
174
+
175
+    public function process(?string $userId, array $input, callable $reportProgress): array {
176
+        return ['output' => $input['input']];
177
+    }
178
+
179
+    public function getInputShapeEnumValues(): array {
180
+        return [];
181
+    }
182
+
183
+    public function getInputShapeDefaults(): array {
184
+        return [];
185
+    }
186
+
187
+    public function getOptionalInputShapeEnumValues(): array {
188
+        return [];
189
+    }
190
+
191
+    public function getOptionalInputShapeDefaults(): array {
192
+        return [];
193
+    }
194
+
195
+    public function getOutputShapeEnumValues(): array {
196
+        return [];
197
+    }
198
+
199
+    public function getOptionalOutputShapeEnumValues(): array {
200
+        return [];
201
+    }
202 202
 }
203 203
 
204 204
 
205 205
 
206 206
 class FailingSyncProvider implements IProvider, ISynchronousProvider {
207
-	public const ERROR_MESSAGE = 'Failure';
208
-	public function getId(): string {
209
-		return 'test:sync:fail';
210
-	}
211
-
212
-	public function getName(): string {
213
-		return self::class;
214
-	}
215
-
216
-	public function getTaskTypeId(): string {
217
-		return TextToText::ID;
218
-	}
219
-
220
-	public function getExpectedRuntime(): int {
221
-		return 10;
222
-	}
223
-
224
-	public function getOptionalInputShape(): array {
225
-		return [
226
-			'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text),
227
-		];
228
-	}
229
-
230
-	public function getOptionalOutputShape(): array {
231
-		return [
232
-			'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text),
233
-		];
234
-	}
235
-
236
-	public function process(?string $userId, array $input, callable $reportProgress): array {
237
-		throw new ProcessingException(self::ERROR_MESSAGE);
238
-	}
239
-
240
-	public function getInputShapeEnumValues(): array {
241
-		return [];
242
-	}
243
-
244
-	public function getInputShapeDefaults(): array {
245
-		return [];
246
-	}
247
-
248
-	public function getOptionalInputShapeEnumValues(): array {
249
-		return [];
250
-	}
251
-
252
-	public function getOptionalInputShapeDefaults(): array {
253
-		return [];
254
-	}
255
-
256
-	public function getOutputShapeEnumValues(): array {
257
-		return [];
258
-	}
259
-
260
-	public function getOptionalOutputShapeEnumValues(): array {
261
-		return [];
262
-	}
207
+    public const ERROR_MESSAGE = 'Failure';
208
+    public function getId(): string {
209
+        return 'test:sync:fail';
210
+    }
211
+
212
+    public function getName(): string {
213
+        return self::class;
214
+    }
215
+
216
+    public function getTaskTypeId(): string {
217
+        return TextToText::ID;
218
+    }
219
+
220
+    public function getExpectedRuntime(): int {
221
+        return 10;
222
+    }
223
+
224
+    public function getOptionalInputShape(): array {
225
+        return [
226
+            'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text),
227
+        ];
228
+    }
229
+
230
+    public function getOptionalOutputShape(): array {
231
+        return [
232
+            'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text),
233
+        ];
234
+    }
235
+
236
+    public function process(?string $userId, array $input, callable $reportProgress): array {
237
+        throw new ProcessingException(self::ERROR_MESSAGE);
238
+    }
239
+
240
+    public function getInputShapeEnumValues(): array {
241
+        return [];
242
+    }
243
+
244
+    public function getInputShapeDefaults(): array {
245
+        return [];
246
+    }
247
+
248
+    public function getOptionalInputShapeEnumValues(): array {
249
+        return [];
250
+    }
251
+
252
+    public function getOptionalInputShapeDefaults(): array {
253
+        return [];
254
+    }
255
+
256
+    public function getOutputShapeEnumValues(): array {
257
+        return [];
258
+    }
259
+
260
+    public function getOptionalOutputShapeEnumValues(): array {
261
+        return [];
262
+    }
263 263
 }
264 264
 
265 265
 
266 266
 class FailingSyncProviderWithUserFacingError implements IProvider, ISynchronousProvider {
267
-	public const ERROR_MESSAGE = 'Failure';
268
-	public const USER_FACING_ERROR_MESSAGE = 'User-facing Failure';
269
-	public function getId(): string {
270
-		return 'test:sync:fail:user-facing';
271
-	}
272
-
273
-	public function getName(): string {
274
-		return self::class;
275
-	}
276
-
277
-	public function getTaskTypeId(): string {
278
-		return TextToText::ID;
279
-	}
280
-
281
-	public function getExpectedRuntime(): int {
282
-		return 10;
283
-	}
284
-
285
-	public function getOptionalInputShape(): array {
286
-		return [
287
-			'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text),
288
-		];
289
-	}
290
-
291
-	public function getOptionalOutputShape(): array {
292
-		return [
293
-			'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text),
294
-		];
295
-	}
296
-
297
-	public function process(?string $userId, array $input, callable $reportProgress): array {
298
-		throw new UserFacingProcessingException(self::ERROR_MESSAGE, userFacingMessage: self::USER_FACING_ERROR_MESSAGE);
299
-	}
300
-
301
-	public function getInputShapeEnumValues(): array {
302
-		return [];
303
-	}
304
-
305
-	public function getInputShapeDefaults(): array {
306
-		return [];
307
-	}
308
-
309
-	public function getOptionalInputShapeEnumValues(): array {
310
-		return [];
311
-	}
312
-
313
-	public function getOptionalInputShapeDefaults(): array {
314
-		return [];
315
-	}
316
-
317
-	public function getOutputShapeEnumValues(): array {
318
-		return [];
319
-	}
320
-
321
-	public function getOptionalOutputShapeEnumValues(): array {
322
-		return [];
323
-	}
267
+    public const ERROR_MESSAGE = 'Failure';
268
+    public const USER_FACING_ERROR_MESSAGE = 'User-facing Failure';
269
+    public function getId(): string {
270
+        return 'test:sync:fail:user-facing';
271
+    }
272
+
273
+    public function getName(): string {
274
+        return self::class;
275
+    }
276
+
277
+    public function getTaskTypeId(): string {
278
+        return TextToText::ID;
279
+    }
280
+
281
+    public function getExpectedRuntime(): int {
282
+        return 10;
283
+    }
284
+
285
+    public function getOptionalInputShape(): array {
286
+        return [
287
+            'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text),
288
+        ];
289
+    }
290
+
291
+    public function getOptionalOutputShape(): array {
292
+        return [
293
+            'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text),
294
+        ];
295
+    }
296
+
297
+    public function process(?string $userId, array $input, callable $reportProgress): array {
298
+        throw new UserFacingProcessingException(self::ERROR_MESSAGE, userFacingMessage: self::USER_FACING_ERROR_MESSAGE);
299
+    }
300
+
301
+    public function getInputShapeEnumValues(): array {
302
+        return [];
303
+    }
304
+
305
+    public function getInputShapeDefaults(): array {
306
+        return [];
307
+    }
308
+
309
+    public function getOptionalInputShapeEnumValues(): array {
310
+        return [];
311
+    }
312
+
313
+    public function getOptionalInputShapeDefaults(): array {
314
+        return [];
315
+    }
316
+
317
+    public function getOutputShapeEnumValues(): array {
318
+        return [];
319
+    }
320
+
321
+    public function getOptionalOutputShapeEnumValues(): array {
322
+        return [];
323
+    }
324 324
 }
325 325
 
326 326
 class BrokenSyncProvider implements IProvider, ISynchronousProvider {
327
-	public function getId(): string {
328
-		return 'test:sync:broken-output';
329
-	}
330
-
331
-	public function getName(): string {
332
-		return self::class;
333
-	}
334
-
335
-	public function getTaskTypeId(): string {
336
-		return TextToText::ID;
337
-	}
338
-
339
-	public function getExpectedRuntime(): int {
340
-		return 10;
341
-	}
342
-
343
-	public function getOptionalInputShape(): array {
344
-		return [
345
-			'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text),
346
-		];
347
-	}
348
-
349
-	public function getOptionalOutputShape(): array {
350
-		return [
351
-			'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text),
352
-		];
353
-	}
354
-
355
-	public function process(?string $userId, array $input, callable $reportProgress): array {
356
-		return [];
357
-	}
358
-
359
-	public function getInputShapeEnumValues(): array {
360
-		return [];
361
-	}
362
-
363
-	public function getInputShapeDefaults(): array {
364
-		return [];
365
-	}
366
-
367
-	public function getOptionalInputShapeEnumValues(): array {
368
-		return [];
369
-	}
370
-
371
-	public function getOptionalInputShapeDefaults(): array {
372
-		return [];
373
-	}
374
-
375
-	public function getOutputShapeEnumValues(): array {
376
-		return [];
377
-	}
378
-
379
-	public function getOptionalOutputShapeEnumValues(): array {
380
-		return [];
381
-	}
327
+    public function getId(): string {
328
+        return 'test:sync:broken-output';
329
+    }
330
+
331
+    public function getName(): string {
332
+        return self::class;
333
+    }
334
+
335
+    public function getTaskTypeId(): string {
336
+        return TextToText::ID;
337
+    }
338
+
339
+    public function getExpectedRuntime(): int {
340
+        return 10;
341
+    }
342
+
343
+    public function getOptionalInputShape(): array {
344
+        return [
345
+            'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text),
346
+        ];
347
+    }
348
+
349
+    public function getOptionalOutputShape(): array {
350
+        return [
351
+            'optionalKey' => new ShapeDescriptor('optional Key', 'AN optional key', EShapeType::Text),
352
+        ];
353
+    }
354
+
355
+    public function process(?string $userId, array $input, callable $reportProgress): array {
356
+        return [];
357
+    }
358
+
359
+    public function getInputShapeEnumValues(): array {
360
+        return [];
361
+    }
362
+
363
+    public function getInputShapeDefaults(): array {
364
+        return [];
365
+    }
366
+
367
+    public function getOptionalInputShapeEnumValues(): array {
368
+        return [];
369
+    }
370
+
371
+    public function getOptionalInputShapeDefaults(): array {
372
+        return [];
373
+    }
374
+
375
+    public function getOutputShapeEnumValues(): array {
376
+        return [];
377
+    }
378
+
379
+    public function getOptionalOutputShapeEnumValues(): array {
380
+        return [];
381
+    }
382 382
 }
383 383
 
384 384
 class SuccessfulTextProcessingSummaryProvider implements \OCP\TextProcessing\IProvider {
385
-	public bool $ran = false;
385
+    public bool $ran = false;
386 386
 
387
-	public function getName(): string {
388
-		return 'TEST Vanilla LLM Provider';
389
-	}
387
+    public function getName(): string {
388
+        return 'TEST Vanilla LLM Provider';
389
+    }
390 390
 
391
-	public function process(string $prompt): string {
392
-		$this->ran = true;
393
-		return $prompt . ' Summarize';
394
-	}
391
+    public function process(string $prompt): string {
392
+        $this->ran = true;
393
+        return $prompt . ' Summarize';
394
+    }
395 395
 
396
-	public function getTaskType(): string {
397
-		return SummaryTaskType::class;
398
-	}
396
+    public function getTaskType(): string {
397
+        return SummaryTaskType::class;
398
+    }
399 399
 }
400 400
 
401 401
 class FailingTextProcessingSummaryProvider implements \OCP\TextProcessing\IProvider {
402
-	public bool $ran = false;
402
+    public bool $ran = false;
403 403
 
404
-	public function getName(): string {
405
-		return 'TEST Vanilla LLM Provider';
406
-	}
404
+    public function getName(): string {
405
+        return 'TEST Vanilla LLM Provider';
406
+    }
407 407
 
408
-	public function process(string $prompt): string {
409
-		$this->ran = true;
410
-		throw new \Exception('ERROR');
411
-	}
408
+    public function process(string $prompt): string {
409
+        $this->ran = true;
410
+        throw new \Exception('ERROR');
411
+    }
412 412
 
413
-	public function getTaskType(): string {
414
-		return SummaryTaskType::class;
415
-	}
413
+    public function getTaskType(): string {
414
+        return SummaryTaskType::class;
415
+    }
416 416
 }
417 417
 
418 418
 class SuccessfulTextToImageProvider implements \OCP\TextToImage\IProvider {
419
-	public bool $ran = false;
420
-
421
-	public function getId(): string {
422
-		return 'test:successful';
423
-	}
424
-
425
-	public function getName(): string {
426
-		return 'TEST Provider';
427
-	}
428
-
429
-	public function generate(string $prompt, array $resources): void {
430
-		$this->ran = true;
431
-		foreach ($resources as $resource) {
432
-			fwrite($resource, 'test');
433
-		}
434
-	}
435
-
436
-	public function getExpectedRuntime(): int {
437
-		return 1;
438
-	}
419
+    public bool $ran = false;
420
+
421
+    public function getId(): string {
422
+        return 'test:successful';
423
+    }
424
+
425
+    public function getName(): string {
426
+        return 'TEST Provider';
427
+    }
428
+
429
+    public function generate(string $prompt, array $resources): void {
430
+        $this->ran = true;
431
+        foreach ($resources as $resource) {
432
+            fwrite($resource, 'test');
433
+        }
434
+    }
435
+
436
+    public function getExpectedRuntime(): int {
437
+        return 1;
438
+    }
439 439
 }
440 440
 
441 441
 class FailingTextToImageProvider implements \OCP\TextToImage\IProvider {
442
-	public bool $ran = false;
442
+    public bool $ran = false;
443 443
 
444
-	public function getId(): string {
445
-		return 'test:failing';
446
-	}
444
+    public function getId(): string {
445
+        return 'test:failing';
446
+    }
447 447
 
448
-	public function getName(): string {
449
-		return 'TEST Provider';
450
-	}
448
+    public function getName(): string {
449
+        return 'TEST Provider';
450
+    }
451 451
 
452
-	public function generate(string $prompt, array $resources): void {
453
-		$this->ran = true;
454
-		throw new \RuntimeException('ERROR');
455
-	}
452
+    public function generate(string $prompt, array $resources): void {
453
+        $this->ran = true;
454
+        throw new \RuntimeException('ERROR');
455
+    }
456 456
 
457
-	public function getExpectedRuntime(): int {
458
-		return 1;
459
-	}
457
+    public function getExpectedRuntime(): int {
458
+        return 1;
459
+    }
460 460
 }
461 461
 
462 462
 class ExternalProvider implements IProvider {
463
-	public const ID = 'event:external:provider';
464
-	public const TASK_TYPE_ID = 'event:external:tasktype';
465
-
466
-	public function getId(): string {
467
-		return self::ID;
468
-	}
469
-	public function getName(): string {
470
-		return 'External Provider via Event';
471
-	}
472
-	public function getTaskTypeId(): string {
473
-		return self::TASK_TYPE_ID;
474
-	}
475
-	public function getExpectedRuntime(): int {
476
-		return 5;
477
-	}
478
-	public function getOptionalInputShape(): array {
479
-		return [];
480
-	}
481
-	public function getOptionalOutputShape(): array {
482
-		return [];
483
-	}
484
-	public function getInputShapeEnumValues(): array {
485
-		return [];
486
-	}
487
-	public function getInputShapeDefaults(): array {
488
-		return [];
489
-	}
490
-	public function getOptionalInputShapeEnumValues(): array {
491
-		return [];
492
-	}
493
-	public function getOptionalInputShapeDefaults(): array {
494
-		return [];
495
-	}
496
-	public function getOutputShapeEnumValues(): array {
497
-		return [];
498
-	}
499
-	public function getOptionalOutputShapeEnumValues(): array {
500
-		return [];
501
-	}
463
+    public const ID = 'event:external:provider';
464
+    public const TASK_TYPE_ID = 'event:external:tasktype';
465
+
466
+    public function getId(): string {
467
+        return self::ID;
468
+    }
469
+    public function getName(): string {
470
+        return 'External Provider via Event';
471
+    }
472
+    public function getTaskTypeId(): string {
473
+        return self::TASK_TYPE_ID;
474
+    }
475
+    public function getExpectedRuntime(): int {
476
+        return 5;
477
+    }
478
+    public function getOptionalInputShape(): array {
479
+        return [];
480
+    }
481
+    public function getOptionalOutputShape(): array {
482
+        return [];
483
+    }
484
+    public function getInputShapeEnumValues(): array {
485
+        return [];
486
+    }
487
+    public function getInputShapeDefaults(): array {
488
+        return [];
489
+    }
490
+    public function getOptionalInputShapeEnumValues(): array {
491
+        return [];
492
+    }
493
+    public function getOptionalInputShapeDefaults(): array {
494
+        return [];
495
+    }
496
+    public function getOutputShapeEnumValues(): array {
497
+        return [];
498
+    }
499
+    public function getOptionalOutputShapeEnumValues(): array {
500
+        return [];
501
+    }
502 502
 }
503 503
 
504 504
 
505 505
 class ExternalTriggerableProvider implements ITriggerableProvider {
506
-	public const ID = 'event:external:provider:triggerable';
507
-	public const TASK_TYPE_ID = TextToText::ID;
508
-
509
-	public function getId(): string {
510
-		return self::ID;
511
-	}
512
-	public function getName(): string {
513
-		return 'External Triggerable Provider via Event';
514
-	}
515
-
516
-	public function getTaskTypeId(): string {
517
-		return self::TASK_TYPE_ID;
518
-	}
519
-
520
-	public function trigger(): void {
521
-	}
522
-	public function getExpectedRuntime(): int {
523
-		return 5;
524
-	}
525
-	public function getOptionalInputShape(): array {
526
-		return [];
527
-	}
528
-	public function getOptionalOutputShape(): array {
529
-		return [];
530
-	}
531
-	public function getInputShapeEnumValues(): array {
532
-		return [];
533
-	}
534
-	public function getInputShapeDefaults(): array {
535
-		return [];
536
-	}
537
-	public function getOptionalInputShapeEnumValues(): array {
538
-		return [];
539
-	}
540
-	public function getOptionalInputShapeDefaults(): array {
541
-		return [];
542
-	}
543
-	public function getOutputShapeEnumValues(): array {
544
-		return [];
545
-	}
546
-	public function getOptionalOutputShapeEnumValues(): array {
547
-		return [];
548
-	}
506
+    public const ID = 'event:external:provider:triggerable';
507
+    public const TASK_TYPE_ID = TextToText::ID;
508
+
509
+    public function getId(): string {
510
+        return self::ID;
511
+    }
512
+    public function getName(): string {
513
+        return 'External Triggerable Provider via Event';
514
+    }
515
+
516
+    public function getTaskTypeId(): string {
517
+        return self::TASK_TYPE_ID;
518
+    }
519
+
520
+    public function trigger(): void {
521
+    }
522
+    public function getExpectedRuntime(): int {
523
+        return 5;
524
+    }
525
+    public function getOptionalInputShape(): array {
526
+        return [];
527
+    }
528
+    public function getOptionalOutputShape(): array {
529
+        return [];
530
+    }
531
+    public function getInputShapeEnumValues(): array {
532
+        return [];
533
+    }
534
+    public function getInputShapeDefaults(): array {
535
+        return [];
536
+    }
537
+    public function getOptionalInputShapeEnumValues(): array {
538
+        return [];
539
+    }
540
+    public function getOptionalInputShapeDefaults(): array {
541
+        return [];
542
+    }
543
+    public function getOutputShapeEnumValues(): array {
544
+        return [];
545
+    }
546
+    public function getOptionalOutputShapeEnumValues(): array {
547
+        return [];
548
+    }
549 549
 }
550 550
 
551 551
 class ConflictingExternalProvider implements IProvider {
552
-	// Same ID as SuccessfulSyncProvider
553
-	public const ID = 'test:sync:success';
554
-	public const TASK_TYPE_ID = 'event:external:tasktype'; // Can be different task type
555
-
556
-	public function getId(): string {
557
-		return self::ID;
558
-	}
559
-	public function getName(): string {
560
-		return 'Conflicting External Provider';
561
-	}
562
-	public function getTaskTypeId(): string {
563
-		return self::TASK_TYPE_ID;
564
-	}
565
-	public function getExpectedRuntime(): int {
566
-		return 50;
567
-	}
568
-	public function getOptionalInputShape(): array {
569
-		return [];
570
-	}
571
-	public function getOptionalOutputShape(): array {
572
-		return [];
573
-	}
574
-	public function getInputShapeEnumValues(): array {
575
-		return [];
576
-	}
577
-	public function getInputShapeDefaults(): array {
578
-		return [];
579
-	}
580
-	public function getOptionalInputShapeEnumValues(): array {
581
-		return [];
582
-	}
583
-	public function getOptionalInputShapeDefaults(): array {
584
-		return [];
585
-	}
586
-	public function getOutputShapeEnumValues(): array {
587
-		return [];
588
-	}
589
-	public function getOptionalOutputShapeEnumValues(): array {
590
-		return [];
591
-	}
552
+    // Same ID as SuccessfulSyncProvider
553
+    public const ID = 'test:sync:success';
554
+    public const TASK_TYPE_ID = 'event:external:tasktype'; // Can be different task type
555
+
556
+    public function getId(): string {
557
+        return self::ID;
558
+    }
559
+    public function getName(): string {
560
+        return 'Conflicting External Provider';
561
+    }
562
+    public function getTaskTypeId(): string {
563
+        return self::TASK_TYPE_ID;
564
+    }
565
+    public function getExpectedRuntime(): int {
566
+        return 50;
567
+    }
568
+    public function getOptionalInputShape(): array {
569
+        return [];
570
+    }
571
+    public function getOptionalOutputShape(): array {
572
+        return [];
573
+    }
574
+    public function getInputShapeEnumValues(): array {
575
+        return [];
576
+    }
577
+    public function getInputShapeDefaults(): array {
578
+        return [];
579
+    }
580
+    public function getOptionalInputShapeEnumValues(): array {
581
+        return [];
582
+    }
583
+    public function getOptionalInputShapeDefaults(): array {
584
+        return [];
585
+    }
586
+    public function getOutputShapeEnumValues(): array {
587
+        return [];
588
+    }
589
+    public function getOptionalOutputShapeEnumValues(): array {
590
+        return [];
591
+    }
592 592
 }
593 593
 
594 594
 class ExternalTaskType implements ITaskType {
595
-	public const ID = 'event:external:tasktype';
596
-
597
-	public function getId(): string {
598
-		return self::ID;
599
-	}
600
-	public function getName(): string {
601
-		return 'External Task Type via Event';
602
-	}
603
-	public function getDescription(): string {
604
-		return 'A task type added via event';
605
-	}
606
-	public function getInputShape(): array {
607
-		return ['external_input' => new ShapeDescriptor('Ext In', '', EShapeType::Text)];
608
-	}
609
-	public function getOutputShape(): array {
610
-		return ['external_output' => new ShapeDescriptor('Ext Out', '', EShapeType::Text)];
611
-	}
595
+    public const ID = 'event:external:tasktype';
596
+
597
+    public function getId(): string {
598
+        return self::ID;
599
+    }
600
+    public function getName(): string {
601
+        return 'External Task Type via Event';
602
+    }
603
+    public function getDescription(): string {
604
+        return 'A task type added via event';
605
+    }
606
+    public function getInputShape(): array {
607
+        return ['external_input' => new ShapeDescriptor('Ext In', '', EShapeType::Text)];
608
+    }
609
+    public function getOutputShape(): array {
610
+        return ['external_output' => new ShapeDescriptor('Ext Out', '', EShapeType::Text)];
611
+    }
612 612
 }
613 613
 
614 614
 class ConflictingExternalTaskType implements ITaskType {
615
-	// Same ID as built-in TextToText
616
-	public const ID = TextToText::ID;
617
-
618
-	public function getId(): string {
619
-		return self::ID;
620
-	}
621
-	public function getName(): string {
622
-		return 'Conflicting External Task Type';
623
-	}
624
-	public function getDescription(): string {
625
-		return 'Overrides built-in TextToText';
626
-	}
627
-	public function getInputShape(): array {
628
-		return ['override_input' => new ShapeDescriptor('Override In', '', EShapeType::Number)];
629
-	}
630
-	public function getOutputShape(): array {
631
-		return ['override_output' => new ShapeDescriptor('Override Out', '', EShapeType::Number)];
632
-	}
615
+    // Same ID as built-in TextToText
616
+    public const ID = TextToText::ID;
617
+
618
+    public function getId(): string {
619
+        return self::ID;
620
+    }
621
+    public function getName(): string {
622
+        return 'Conflicting External Task Type';
623
+    }
624
+    public function getDescription(): string {
625
+        return 'Overrides built-in TextToText';
626
+    }
627
+    public function getInputShape(): array {
628
+        return ['override_input' => new ShapeDescriptor('Override In', '', EShapeType::Number)];
629
+    }
630
+    public function getOutputShape(): array {
631
+        return ['override_output' => new ShapeDescriptor('Override Out', '', EShapeType::Number)];
632
+    }
633 633
 }
634 634
 
635 635
 #[\PHPUnit\Framework\Attributes\Group('DB')]
636 636
 class TaskProcessingTest extends \Test\TestCase {
637
-	private IManager $manager;
638
-	private Coordinator $coordinator;
639
-	private array $providers;
640
-	private IServerContainer $serverContainer;
641
-	private IEventDispatcher $eventDispatcher;
642
-	private RegistrationContext $registrationContext;
643
-	private TaskMapper $taskMapper;
644
-	private IJobList $jobList;
645
-	private IUserMountCache $userMountCache;
646
-	private IRootFolder $rootFolder;
647
-	private IConfig $config;
648
-	private IAppConfig $appConfig;
649
-
650
-	public const TEST_USER = 'testuser';
651
-
652
-	protected function setUp(): void {
653
-		parent::setUp();
654
-
655
-		$this->providers = [
656
-			SuccessfulSyncProvider::class => new SuccessfulSyncProvider(),
657
-			FailingSyncProvider::class => new FailingSyncProvider(),
658
-			FailingSyncProviderWithUserFacingError::class => new FailingSyncProviderWithUserFacingError(),
659
-			BrokenSyncProvider::class => new BrokenSyncProvider(),
660
-			AsyncProvider::class => new AsyncProvider(),
661
-			AudioToImage::class => new AudioToImage(),
662
-			SuccessfulTextProcessingSummaryProvider::class => new SuccessfulTextProcessingSummaryProvider(),
663
-			FailingTextProcessingSummaryProvider::class => new FailingTextProcessingSummaryProvider(),
664
-			SuccessfulTextToImageProvider::class => new SuccessfulTextToImageProvider(),
665
-			FailingTextToImageProvider::class => new FailingTextToImageProvider(),
666
-			ExternalProvider::class => new ExternalProvider(),
667
-			ExternalTriggerableProvider::class => new ExternalTriggerableProvider(),
668
-			ConflictingExternalProvider::class => new ConflictingExternalProvider(),
669
-			ExternalTaskType::class => new ExternalTaskType(),
670
-			ConflictingExternalTaskType::class => new ConflictingExternalTaskType(),
671
-		];
672
-
673
-		$userManager = Server::get(IUserManager::class);
674
-		if (!$userManager->userExists(self::TEST_USER)) {
675
-			$userManager->createUser(self::TEST_USER, 'test');
676
-		}
677
-
678
-		$this->serverContainer = $this->createMock(IServerContainer::class);
679
-		$this->serverContainer->expects($this->any())->method('get')->willReturnCallback(function ($class) {
680
-			return $this->providers[$class];
681
-		});
682
-
683
-		$this->eventDispatcher = new EventDispatcher(
684
-			new \Symfony\Component\EventDispatcher\EventDispatcher(),
685
-			$this->serverContainer,
686
-			Server::get(LoggerInterface::class),
687
-		);
688
-
689
-		$this->registrationContext = $this->createMock(RegistrationContext::class);
690
-		$this->coordinator = $this->createMock(Coordinator::class);
691
-		$this->coordinator->expects($this->any())->method('getRegistrationContext')->willReturn($this->registrationContext);
692
-
693
-		$this->rootFolder = Server::get(IRootFolder::class);
694
-
695
-		$this->taskMapper = Server::get(TaskMapper::class);
696
-
697
-		$this->jobList = $this->createPartialMock(DummyJobList::class, ['add']);
698
-		$this->jobList->expects($this->any())->method('add')->willReturnCallback(function (): void {
699
-		});
700
-
701
-		$this->eventDispatcher = $this->createMock(IEventDispatcher::class);
702
-		$this->configureEventDispatcherMock();
703
-
704
-		$text2imageManager = new \OC\TextToImage\Manager(
705
-			$this->serverContainer,
706
-			$this->coordinator,
707
-			Server::get(LoggerInterface::class),
708
-			$this->jobList,
709
-			Server::get(\OC\TextToImage\Db\TaskMapper::class),
710
-			Server::get(IConfig::class),
711
-			Server::get(IAppDataFactory::class),
712
-		);
713
-
714
-		$this->userMountCache = $this->createMock(IUserMountCache::class);
715
-		$this->config = Server::get(IConfig::class);
716
-		$this->appConfig = Server::get(IAppConfig::class);
717
-		$this->manager = new Manager(
718
-			$this->appConfig,
719
-			$this->coordinator,
720
-			$this->serverContainer,
721
-			Server::get(LoggerInterface::class),
722
-			$this->taskMapper,
723
-			$this->jobList,
724
-			$this->eventDispatcher,
725
-			Server::get(IAppDataFactory::class),
726
-			Server::get(IRootFolder::class),
727
-			$text2imageManager,
728
-			$this->userMountCache,
729
-			Server::get(IClientService::class),
730
-			Server::get(IAppManager::class),
731
-			$userManager,
732
-			Server::get(IUserSession::class),
733
-			Server::get(ICacheFactory::class),
734
-			Server::get(IFactory::class),
735
-		);
736
-	}
737
-
738
-	private function getFile(string $name, string $content): File {
739
-		$folder = $this->rootFolder->getUserFolder(self::TEST_USER);
740
-		$file = $folder->newFile($name, $content);
741
-		return $file;
742
-	}
743
-
744
-	public function testShouldNotHaveAnyProviders(): void {
745
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([]);
746
-		self::assertCount(0, $this->manager->getAvailableTaskTypes());
747
-		self::assertCount(0, $this->manager->getAvailableTaskTypeIds());
748
-		self::assertFalse($this->manager->hasProviders());
749
-		self::expectException(PreConditionNotMetException::class);
750
-		$this->manager->scheduleTask(new Task(TextToText::ID, ['input' => 'Hello'], 'test', null));
751
-	}
752
-
753
-	public function testProviderShouldBeRegisteredAndTaskTypeDisabled(): void {
754
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
755
-			new ServiceRegistration('test', SuccessfulSyncProvider::class)
756
-		]);
757
-		$taskProcessingTypeSettings = [
758
-			TextToText::ID => false,
759
-		];
760
-		$this->appConfig->setValueString('core', 'ai.taskprocessing_type_preferences', json_encode($taskProcessingTypeSettings), lazy: true);
761
-		self::assertCount(0, $this->manager->getAvailableTaskTypes());
762
-		self::assertCount(1, $this->manager->getAvailableTaskTypes(true));
763
-		self::assertCount(0, $this->manager->getAvailableTaskTypeIds());
764
-		self::assertCount(1, $this->manager->getAvailableTaskTypeIds(true));
765
-		self::assertTrue($this->manager->hasProviders());
766
-		self::expectException(PreConditionNotMetException::class);
767
-		$this->manager->scheduleTask(new Task(TextToText::ID, ['input' => 'Hello'], 'test', null));
768
-	}
769
-
770
-
771
-	public function testProviderShouldBeRegisteredAndTaskFailValidation(): void {
772
-		$this->appConfig->setValueString('core', 'ai.taskprocessing_type_preferences', '', lazy: true);
773
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
774
-			new ServiceRegistration('test', BrokenSyncProvider::class)
775
-		]);
776
-		self::assertCount(1, $this->manager->getAvailableTaskTypes());
777
-		self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
778
-		self::assertTrue($this->manager->hasProviders());
779
-		$task = new Task(TextToText::ID, ['wrongInputKey' => 'Hello'], 'test', null);
780
-		self::assertNull($task->getId());
781
-		self::expectException(ValidationException::class);
782
-		$this->manager->scheduleTask($task);
783
-	}
784
-
785
-	public function testProviderShouldBeRegisteredAndTaskWithFilesFailValidation(): void {
786
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingTaskTypes')->willReturn([
787
-			new ServiceRegistration('test', AudioToImage::class)
788
-		]);
789
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
790
-			new ServiceRegistration('test', AsyncProvider::class)
791
-		]);
792
-		$user = $this->createMock(IUser::class);
793
-		$user->expects($this->any())->method('getUID')->willReturn(null);
794
-		$mount = $this->createMock(ICachedMountInfo::class);
795
-		$mount->expects($this->any())->method('getUser')->willReturn($user);
796
-		$this->userMountCache->expects($this->any())->method('getMountsForFileId')->willReturn([$mount]);
797
-
798
-		self::assertCount(1, $this->manager->getAvailableTaskTypes());
799
-		self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
800
-		self::assertTrue($this->manager->hasProviders());
801
-
802
-		$audioId = $this->getFile('audioInput', 'Hello')->getId();
803
-		$task = new Task(AudioToImage::ID, ['audio' => $audioId], 'test', null);
804
-		self::assertNull($task->getId());
805
-		self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus());
806
-		self::expectException(UnauthorizedException::class);
807
-		$this->manager->scheduleTask($task);
808
-	}
809
-
810
-	public function testProviderShouldBeRegisteredAndFail(): void {
811
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
812
-			new ServiceRegistration('test', FailingSyncProvider::class)
813
-		]);
814
-		self::assertCount(1, $this->manager->getAvailableTaskTypes());
815
-		self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
816
-		self::assertTrue($this->manager->hasProviders());
817
-		$task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null);
818
-		self::assertNull($task->getId());
819
-		self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus());
820
-		$this->manager->scheduleTask($task);
821
-		self::assertNotNull($task->getId());
822
-		self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus());
823
-
824
-		$this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskFailedEvent::class));
825
-
826
-		$backgroundJob = new SynchronousBackgroundJob(
827
-			Server::get(ITimeFactory::class),
828
-			$this->manager,
829
-			$this->jobList,
830
-			Server::get(LoggerInterface::class),
831
-		);
832
-		$backgroundJob->start($this->jobList);
833
-
834
-		$task = $this->manager->getTask($task->getId());
835
-		self::assertEquals(Task::STATUS_FAILED, $task->getStatus());
836
-		self::assertEquals(FailingSyncProvider::ERROR_MESSAGE, $task->getErrorMessage());
837
-	}
838
-
839
-	public function testProviderShouldBeRegisteredAndFailWithUserFacingMessage(): void {
840
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
841
-			new ServiceRegistration('test', FailingSyncProviderWithUserFacingError::class)
842
-		]);
843
-		self::assertCount(1, $this->manager->getAvailableTaskTypes());
844
-		self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
845
-		self::assertTrue($this->manager->hasProviders());
846
-		$task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null);
847
-		self::assertNull($task->getId());
848
-		self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus());
849
-		$this->manager->scheduleTask($task);
850
-		self::assertNotNull($task->getId());
851
-		self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus());
852
-
853
-		$this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskFailedEvent::class));
854
-
855
-		$backgroundJob = new SynchronousBackgroundJob(
856
-			Server::get(ITimeFactory::class),
857
-			$this->manager,
858
-			$this->jobList,
859
-			Server::get(LoggerInterface::class),
860
-		);
861
-		$backgroundJob->start($this->jobList);
862
-
863
-		$task = $this->manager->getTask($task->getId());
864
-		self::assertEquals(Task::STATUS_FAILED, $task->getStatus());
865
-		self::assertEquals(FailingSyncProviderWithUserFacingError::ERROR_MESSAGE, $task->getErrorMessage());
866
-		self::assertEquals(FailingSyncProviderWithUserFacingError::USER_FACING_ERROR_MESSAGE, $task->getUserFacingErrorMessage());
867
-	}
868
-
869
-	public function testProviderShouldBeRegisteredAndFailOutputValidation(): void {
870
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
871
-			new ServiceRegistration('test', BrokenSyncProvider::class)
872
-		]);
873
-		self::assertCount(1, $this->manager->getAvailableTaskTypes());
874
-		self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
875
-		self::assertTrue($this->manager->hasProviders());
876
-		$task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null);
877
-		self::assertNull($task->getId());
878
-		self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus());
879
-		$this->manager->scheduleTask($task);
880
-		self::assertNotNull($task->getId());
881
-		self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus());
882
-
883
-		$this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskFailedEvent::class));
884
-
885
-		$backgroundJob = new SynchronousBackgroundJob(
886
-			Server::get(ITimeFactory::class),
887
-			$this->manager,
888
-			$this->jobList,
889
-			Server::get(LoggerInterface::class),
890
-		);
891
-		$backgroundJob->start($this->jobList);
892
-
893
-		$task = $this->manager->getTask($task->getId());
894
-		self::assertEquals(Task::STATUS_FAILED, $task->getStatus());
895
-		self::assertEquals('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', $task->getErrorMessage());
896
-	}
897
-
898
-	public function testProviderShouldBeRegisteredAndRun(): void {
899
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
900
-			new ServiceRegistration('test', SuccessfulSyncProvider::class)
901
-		]);
902
-		self::assertCount(1, $this->manager->getAvailableTaskTypes());
903
-		self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
904
-		$taskTypeStruct = $this->manager->getAvailableTaskTypes()[array_keys($this->manager->getAvailableTaskTypes())[0]];
905
-		self::assertTrue(isset($taskTypeStruct['inputShape']['input']));
906
-		self::assertEquals(EShapeType::Text, $taskTypeStruct['inputShape']['input']->getShapeType());
907
-		self::assertTrue(isset($taskTypeStruct['optionalInputShape']['optionalKey']));
908
-		self::assertEquals(EShapeType::Text, $taskTypeStruct['optionalInputShape']['optionalKey']->getShapeType());
909
-		self::assertTrue(isset($taskTypeStruct['outputShape']['output']));
910
-		self::assertEquals(EShapeType::Text, $taskTypeStruct['outputShape']['output']->getShapeType());
911
-		self::assertTrue(isset($taskTypeStruct['optionalOutputShape']['optionalKey']));
912
-		self::assertEquals(EShapeType::Text, $taskTypeStruct['optionalOutputShape']['optionalKey']->getShapeType());
913
-
914
-		self::assertTrue($this->manager->hasProviders());
915
-		$task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null);
916
-		self::assertNull($task->getId());
917
-		self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus());
918
-		$this->manager->scheduleTask($task);
919
-		self::assertNotNull($task->getId());
920
-		self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus());
921
-
922
-		// Task object retrieved from db is up-to-date
923
-		$task2 = $this->manager->getTask($task->getId());
924
-		self::assertEquals($task->getId(), $task2->getId());
925
-		self::assertEquals(['input' => 'Hello'], $task2->getInput());
926
-		self::assertNull($task2->getOutput());
927
-		self::assertEquals(Task::STATUS_SCHEDULED, $task2->getStatus());
928
-
929
-		$this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class));
930
-
931
-		$backgroundJob = new SynchronousBackgroundJob(
932
-			Server::get(ITimeFactory::class),
933
-			$this->manager,
934
-			$this->jobList,
935
-			Server::get(LoggerInterface::class),
936
-		);
937
-		$backgroundJob->start($this->jobList);
938
-
939
-		$task = $this->manager->getTask($task->getId());
940
-		self::assertEquals(Task::STATUS_SUCCESSFUL, $task->getStatus(), 'Status is ' . $task->getStatus() . ' with error message: ' . $task->getErrorMessage());
941
-		self::assertEquals(['output' => 'Hello'], $task->getOutput());
942
-		self::assertEquals(1, $task->getProgress());
943
-	}
944
-
945
-	public function testTaskTypeExplicitlyEnabled(): void {
946
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
947
-			new ServiceRegistration('test', SuccessfulSyncProvider::class)
948
-		]);
949
-
950
-		$taskProcessingTypeSettings = [
951
-			TextToText::ID => true,
952
-		];
953
-		$this->appConfig->setValueString('core', 'ai.taskprocessing_type_preferences', json_encode($taskProcessingTypeSettings), lazy: true);
954
-
955
-		self::assertCount(1, $this->manager->getAvailableTaskTypes());
956
-		self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
957
-
958
-		self::assertTrue($this->manager->hasProviders());
959
-		$task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null);
960
-		self::assertNull($task->getId());
961
-		self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus());
962
-		$this->manager->scheduleTask($task);
963
-		self::assertNotNull($task->getId());
964
-		self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus());
965
-
966
-		$this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class));
967
-
968
-		$backgroundJob = new SynchronousBackgroundJob(
969
-			Server::get(ITimeFactory::class),
970
-			$this->manager,
971
-			$this->jobList,
972
-			Server::get(LoggerInterface::class),
973
-		);
974
-		$backgroundJob->start($this->jobList);
975
-
976
-		$task = $this->manager->getTask($task->getId());
977
-		self::assertEquals(Task::STATUS_SUCCESSFUL, $task->getStatus(), 'Status is ' . $task->getStatus() . ' with error message: ' . $task->getErrorMessage());
978
-		self::assertEquals(['output' => 'Hello'], $task->getOutput());
979
-		self::assertEquals(1, $task->getProgress());
980
-	}
981
-
982
-	public function testAsyncProviderWithFilesShouldBeRegisteredAndRunReturningRawFileData(): void {
983
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingTaskTypes')->willReturn([
984
-			new ServiceRegistration('test', AudioToImage::class)
985
-		]);
986
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
987
-			new ServiceRegistration('test', AsyncProvider::class)
988
-		]);
989
-
990
-		$user = $this->createMock(IUser::class);
991
-		$user->expects($this->any())->method('getUID')->willReturn('testuser');
992
-		$mount = $this->createMock(ICachedMountInfo::class);
993
-		$mount->expects($this->any())->method('getUser')->willReturn($user);
994
-		$this->userMountCache->expects($this->any())->method('getMountsForFileId')->willReturn([$mount]);
995
-
996
-		self::assertCount(1, $this->manager->getAvailableTaskTypes());
997
-		self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
998
-
999
-		self::assertTrue($this->manager->hasProviders());
1000
-		$audioId = $this->getFile('audioInput', 'Hello')->getId();
1001
-		$task = new Task(AudioToImage::ID, ['audio' => $audioId], 'test', 'testuser');
1002
-		self::assertNull($task->getId());
1003
-		self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus());
1004
-		$this->manager->scheduleTask($task);
1005
-		self::assertNotNull($task->getId());
1006
-		self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus());
1007
-
1008
-		// Task object retrieved from db is up-to-date
1009
-		$task2 = $this->manager->getTask($task->getId());
1010
-		self::assertEquals($task->getId(), $task2->getId());
1011
-		self::assertEquals(['audio' => $audioId], $task2->getInput());
1012
-		self::assertNull($task2->getOutput());
1013
-		self::assertEquals(Task::STATUS_SCHEDULED, $task2->getStatus());
1014
-
1015
-		$this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class));
1016
-
1017
-		$this->manager->setTaskProgress($task2->getId(), 0.1);
1018
-		$input = $this->manager->prepareInputData($task2);
1019
-		self::assertTrue(isset($input['audio']));
1020
-		self::assertInstanceOf(File::class, $input['audio']);
1021
-		self::assertEquals($audioId, $input['audio']->getId());
1022
-
1023
-		$this->manager->setTaskResult($task2->getId(), null, ['spectrogram' => 'World']);
1024
-
1025
-		$task = $this->manager->getTask($task->getId());
1026
-		self::assertEquals(Task::STATUS_SUCCESSFUL, $task->getStatus());
1027
-		self::assertEquals(1, $task->getProgress());
1028
-		self::assertTrue(isset($task->getOutput()['spectrogram']));
1029
-		$node = $this->rootFolder->getFirstNodeByIdInPath($task->getOutput()['spectrogram'], '/' . $this->rootFolder->getAppDataDirectoryName() . '/');
1030
-		self::assertNotNull($node);
1031
-		self::assertInstanceOf(File::class, $node);
1032
-		self::assertEquals('World', $node->getContent());
1033
-	}
1034
-
1035
-	public function testAsyncProviderWithFilesShouldBeRegisteredAndRunReturningFileIds(): void {
1036
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingTaskTypes')->willReturn([
1037
-			new ServiceRegistration('test', AudioToImage::class)
1038
-		]);
1039
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
1040
-			new ServiceRegistration('test', AsyncProvider::class)
1041
-		]);
1042
-		$user = $this->createMock(IUser::class);
1043
-		$user->expects($this->any())->method('getUID')->willReturn('testuser');
1044
-		$mount = $this->createMock(ICachedMountInfo::class);
1045
-		$mount->expects($this->any())->method('getUser')->willReturn($user);
1046
-		$this->userMountCache->expects($this->any())->method('getMountsForFileId')->willReturn([$mount]);
1047
-		self::assertCount(1, $this->manager->getAvailableTaskTypes());
1048
-		self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
1049
-
1050
-		self::assertTrue($this->manager->hasProviders());
1051
-		$audioId = $this->getFile('audioInput', 'Hello')->getId();
1052
-		$task = new Task(AudioToImage::ID, ['audio' => $audioId], 'test', 'testuser');
1053
-		self::assertNull($task->getId());
1054
-		self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus());
1055
-		$this->manager->scheduleTask($task);
1056
-		self::assertNotNull($task->getId());
1057
-		self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus());
1058
-
1059
-		// Task object retrieved from db is up-to-date
1060
-		$task2 = $this->manager->getTask($task->getId());
1061
-		self::assertEquals($task->getId(), $task2->getId());
1062
-		self::assertEquals(['audio' => $audioId], $task2->getInput());
1063
-		self::assertNull($task2->getOutput());
1064
-		self::assertEquals(Task::STATUS_SCHEDULED, $task2->getStatus());
1065
-
1066
-		$this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class));
1067
-
1068
-		$this->manager->setTaskProgress($task2->getId(), 0.1);
1069
-		$input = $this->manager->prepareInputData($task2);
1070
-		self::assertTrue(isset($input['audio']));
1071
-		self::assertInstanceOf(File::class, $input['audio']);
1072
-		self::assertEquals($audioId, $input['audio']->getId());
1073
-
1074
-		$outputFileId = $this->getFile('audioOutput', 'World')->getId();
1075
-
1076
-		$this->manager->setTaskResult($task2->getId(), null, ['spectrogram' => $outputFileId], true);
1077
-
1078
-		$task = $this->manager->getTask($task->getId());
1079
-		self::assertEquals(Task::STATUS_SUCCESSFUL, $task->getStatus());
1080
-		self::assertEquals(1, $task->getProgress());
1081
-		self::assertTrue(isset($task->getOutput()['spectrogram']));
1082
-		$node = $this->rootFolder->getFirstNodeById($task->getOutput()['spectrogram']);
1083
-		self::assertNotNull($node, 'fileId:' . $task->getOutput()['spectrogram']);
1084
-		self::assertInstanceOf(File::class, $node);
1085
-		self::assertEquals('World', $node->getContent());
1086
-	}
1087
-
1088
-	public function testNonexistentTask(): void {
1089
-		$this->expectException(NotFoundException::class);
1090
-		$this->manager->getTask(2147483646);
1091
-	}
1092
-
1093
-	public function testOldTasksShouldBeCleanedUp(): void {
1094
-		$currentTime = new \DateTime('now');
1095
-		$timeFactory = $this->createMock(ITimeFactory::class);
1096
-		$timeFactory->expects($this->any())->method('getDateTime')->willReturnCallback(fn () => $currentTime);
1097
-		$timeFactory->expects($this->any())->method('getTime')->willReturnCallback(fn () => $currentTime->getTimestamp());
1098
-
1099
-		$this->taskMapper = new TaskMapper(
1100
-			Server::get(IDBConnection::class),
1101
-			$timeFactory,
1102
-		);
1103
-
1104
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
1105
-			new ServiceRegistration('test', SuccessfulSyncProvider::class)
1106
-		]);
1107
-		self::assertCount(1, $this->manager->getAvailableTaskTypes());
1108
-		self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
1109
-		self::assertTrue($this->manager->hasProviders());
1110
-		$task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null);
1111
-		$this->manager->scheduleTask($task);
1112
-
1113
-		$this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class));
1114
-
1115
-		$backgroundJob = new SynchronousBackgroundJob(
1116
-			Server::get(ITimeFactory::class),
1117
-			$this->manager,
1118
-			$this->jobList,
1119
-			Server::get(LoggerInterface::class),
1120
-		);
1121
-		$backgroundJob->start($this->jobList);
1122
-
1123
-		$task = $this->manager->getTask($task->getId());
1124
-
1125
-		$currentTime = $currentTime->add(new \DateInterval('P1Y'));
1126
-		// run background job
1127
-		$bgJob = new RemoveOldTasksBackgroundJob(
1128
-			$timeFactory,
1129
-			$this->manager,
1130
-			$this->taskMapper,
1131
-			Server::get(LoggerInterface::class),
1132
-			Server::get(IAppDataFactory::class),
1133
-		);
1134
-		$bgJob->setArgument([]);
1135
-		$bgJob->start($this->jobList);
1136
-
1137
-		$this->expectException(NotFoundException::class);
1138
-		$this->manager->getTask($task->getId());
1139
-	}
1140
-
1141
-	public function testShouldTransparentlyHandleTextProcessingProviders(): void {
1142
-		$this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([
1143
-			new ServiceRegistration('test', SuccessfulTextProcessingSummaryProvider::class)
1144
-		]);
1145
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
1146
-		]);
1147
-		$taskTypes = $this->manager->getAvailableTaskTypes();
1148
-		self::assertCount(1, $taskTypes);
1149
-		self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
1150
-		self::assertTrue(isset($taskTypes[TextToTextSummary::ID]));
1151
-		self::assertTrue($this->manager->hasProviders());
1152
-		$task = new Task(TextToTextSummary::ID, ['input' => 'Hello'], 'test', null);
1153
-		$this->manager->scheduleTask($task);
1154
-
1155
-		$this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class));
1156
-
1157
-		$backgroundJob = new SynchronousBackgroundJob(
1158
-			Server::get(ITimeFactory::class),
1159
-			$this->manager,
1160
-			$this->jobList,
1161
-			Server::get(LoggerInterface::class),
1162
-		);
1163
-		$backgroundJob->start($this->jobList);
1164
-
1165
-		$task = $this->manager->getTask($task->getId());
1166
-		self::assertEquals(Task::STATUS_SUCCESSFUL, $task->getStatus());
1167
-		self::assertIsArray($task->getOutput());
1168
-		self::assertTrue(isset($task->getOutput()['output']));
1169
-		self::assertEquals('Hello Summarize', $task->getOutput()['output']);
1170
-		self::assertTrue($this->providers[SuccessfulTextProcessingSummaryProvider::class]->ran);
1171
-	}
1172
-
1173
-	public function testShouldTransparentlyHandleFailingTextProcessingProviders(): void {
1174
-		$this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([
1175
-			new ServiceRegistration('test', FailingTextProcessingSummaryProvider::class)
1176
-		]);
1177
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
1178
-		]);
1179
-		$taskTypes = $this->manager->getAvailableTaskTypes();
1180
-		self::assertCount(1, $taskTypes);
1181
-		self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
1182
-		self::assertTrue(isset($taskTypes[TextToTextSummary::ID]));
1183
-		self::assertTrue($this->manager->hasProviders());
1184
-		$task = new Task(TextToTextSummary::ID, ['input' => 'Hello'], 'test', null);
1185
-		$this->manager->scheduleTask($task);
1186
-
1187
-		$this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskFailedEvent::class));
1188
-
1189
-		$backgroundJob = new SynchronousBackgroundJob(
1190
-			Server::get(ITimeFactory::class),
1191
-			$this->manager,
1192
-			$this->jobList,
1193
-			Server::get(LoggerInterface::class),
1194
-		);
1195
-		$backgroundJob->start($this->jobList);
1196
-
1197
-		$task = $this->manager->getTask($task->getId());
1198
-		self::assertEquals(Task::STATUS_FAILED, $task->getStatus());
1199
-		self::assertTrue($task->getOutput() === null);
1200
-		self::assertEquals('ERROR', $task->getErrorMessage());
1201
-		self::assertTrue($this->providers[FailingTextProcessingSummaryProvider::class]->ran);
1202
-	}
1203
-
1204
-	public function testShouldTransparentlyHandleText2ImageProviders(): void {
1205
-		$this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([
1206
-			new ServiceRegistration('test', SuccessfulTextToImageProvider::class)
1207
-		]);
1208
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
1209
-		]);
1210
-		$taskTypes = $this->manager->getAvailableTaskTypes();
1211
-		self::assertCount(1, $taskTypes);
1212
-		self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
1213
-		self::assertTrue(isset($taskTypes[TextToImage::ID]));
1214
-		self::assertTrue($this->manager->hasProviders());
1215
-		$task = new Task(TextToImage::ID, ['input' => 'Hello', 'numberOfImages' => 3], 'test', null);
1216
-		$this->manager->scheduleTask($task);
1217
-
1218
-		$this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class));
1219
-
1220
-		$backgroundJob = new SynchronousBackgroundJob(
1221
-			Server::get(ITimeFactory::class),
1222
-			$this->manager,
1223
-			$this->jobList,
1224
-			Server::get(LoggerInterface::class),
1225
-		);
1226
-		$backgroundJob->start($this->jobList);
1227
-
1228
-		$task = $this->manager->getTask($task->getId());
1229
-		self::assertEquals(Task::STATUS_SUCCESSFUL, $task->getStatus());
1230
-		self::assertIsArray($task->getOutput());
1231
-		self::assertTrue(isset($task->getOutput()['images']));
1232
-		self::assertIsArray($task->getOutput()['images']);
1233
-		self::assertCount(3, $task->getOutput()['images']);
1234
-		self::assertTrue($this->providers[SuccessfulTextToImageProvider::class]->ran);
1235
-		$node = $this->rootFolder->getFirstNodeByIdInPath($task->getOutput()['images'][0], '/' . $this->rootFolder->getAppDataDirectoryName() . '/');
1236
-		self::assertNotNull($node);
1237
-		self::assertInstanceOf(File::class, $node);
1238
-		self::assertEquals('test', $node->getContent());
1239
-	}
1240
-
1241
-	public function testShouldTransparentlyHandleFailingText2ImageProviders(): void {
1242
-		$this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([
1243
-			new ServiceRegistration('test', FailingTextToImageProvider::class)
1244
-		]);
1245
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
1246
-		]);
1247
-		$taskTypes = $this->manager->getAvailableTaskTypes();
1248
-		self::assertCount(1, $taskTypes);
1249
-		self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
1250
-		self::assertTrue(isset($taskTypes[TextToImage::ID]));
1251
-		self::assertTrue($this->manager->hasProviders());
1252
-		$task = new Task(TextToImage::ID, ['input' => 'Hello', 'numberOfImages' => 3], 'test', null);
1253
-		$this->manager->scheduleTask($task);
1254
-
1255
-		$this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskFailedEvent::class));
1256
-
1257
-		$backgroundJob = new SynchronousBackgroundJob(
1258
-			Server::get(ITimeFactory::class),
1259
-			$this->manager,
1260
-			$this->jobList,
1261
-			Server::get(LoggerInterface::class),
1262
-		);
1263
-		$backgroundJob->start($this->jobList);
1264
-
1265
-		$task = $this->manager->getTask($task->getId());
1266
-		self::assertEquals(Task::STATUS_FAILED, $task->getStatus());
1267
-		self::assertTrue($task->getOutput() === null);
1268
-		self::assertEquals('ERROR', $task->getErrorMessage());
1269
-		self::assertTrue($this->providers[FailingTextToImageProvider::class]->ran);
1270
-	}
1271
-
1272
-	public function testMergeProvidersLocalAndEvent() {
1273
-		// Arrange: Local provider registered, DIFFERENT external provider via event
1274
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
1275
-			new ServiceRegistration('test', SuccessfulSyncProvider::class)
1276
-		]);
1277
-		$this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([]);
1278
-		$this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([]);
1279
-		$this->registrationContext->expects($this->any())->method('getSpeechToTextProviders')->willReturn([]);
1280
-
1281
-		$externalProvider = new ExternalProvider(); // ID = 'event:external:provider'
1282
-		$this->configureEventDispatcherMock(providersToAdd: [$externalProvider]);
1283
-		$this->manager = $this->createManagerInstance();
1284
-
1285
-		// Act
1286
-		$providers = $this->manager->getProviders();
1287
-
1288
-		// Assert: Both providers should be present
1289
-		self::assertArrayHasKey(SuccessfulSyncProvider::ID, $providers);
1290
-		self::assertInstanceOf(SuccessfulSyncProvider::class, $providers[SuccessfulSyncProvider::ID]);
1291
-		self::assertArrayHasKey(ExternalProvider::ID, $providers);
1292
-		self::assertInstanceOf(ExternalProvider::class, $providers[ExternalProvider::ID]);
1293
-		self::assertCount(2, $providers);
1294
-	}
1295
-
1296
-	public function testGetProvidersIncludesExternalViaEvent() {
1297
-		// Arrange: No local providers, one external provider via event
1298
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([]);
1299
-		$this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([]);
1300
-		$this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([]);
1301
-		$this->registrationContext->expects($this->any())->method('getSpeechToTextProviders')->willReturn([]);
1302
-
1303
-
1304
-		$externalProvider = new ExternalProvider();
1305
-		$this->configureEventDispatcherMock(providersToAdd: [$externalProvider]);
1306
-		$this->manager = $this->createManagerInstance(); // Create manager with configured mocks
1307
-
1308
-		// Act
1309
-		$providers = $this->manager->getProviders(); // Returns ID-indexed array
1310
-
1311
-		// Assert
1312
-		self::assertArrayHasKey(ExternalProvider::ID, $providers);
1313
-		self::assertInstanceOf(ExternalProvider::class, $providers[ExternalProvider::ID]);
1314
-		self::assertCount(1, $providers);
1315
-		self::assertTrue($this->manager->hasProviders());
1316
-	}
1317
-
1318
-	public function testGetAvailableTaskTypesIncludesExternalViaEvent() {
1319
-		// Arrange: No local types/providers, one external type and provider via event
1320
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([]);
1321
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingTaskTypes')->willReturn([]);
1322
-		$this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([]);
1323
-		$this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([]);
1324
-		$this->registrationContext->expects($this->any())->method('getSpeechToTextProviders')->willReturn([]);
1325
-
1326
-		$externalProvider = new ExternalProvider(); // Provides ExternalTaskType
1327
-		$externalTaskType = new ExternalTaskType();
1328
-		$this->configureEventDispatcherMock(
1329
-			providersToAdd: [$externalProvider],
1330
-			taskTypesToAdd: [$externalTaskType]
1331
-		);
1332
-		$this->manager = $this->createManagerInstance();
1333
-
1334
-		// Act
1335
-		$availableTypes = $this->manager->getAvailableTaskTypes();
1336
-
1337
-		// Assert
1338
-		self::assertArrayHasKey(ExternalTaskType::ID, $availableTypes);
1339
-		self::assertContains(ExternalTaskType::ID, $this->manager->getAvailableTaskTypeIds());
1340
-		self::assertEquals(ExternalTaskType::ID, $externalProvider->getTaskTypeId(), 'Test Sanity: Provider must handle the Task Type');
1341
-		self::assertEquals('External Task Type via Event', $availableTypes[ExternalTaskType::ID]['name']);
1342
-		// Check if shapes match the external type/provider
1343
-		self::assertArrayHasKey('external_input', $availableTypes[ExternalTaskType::ID]['inputShape']);
1344
-		self::assertArrayHasKey('external_output', $availableTypes[ExternalTaskType::ID]['outputShape']);
1345
-		self::assertEmpty($availableTypes[ExternalTaskType::ID]['optionalInputShape']); // From ExternalProvider
1346
-	}
1347
-
1348
-	public function testLocalProviderWinsConflictWithEvent() {
1349
-		// Arrange: Local provider registered, conflicting external provider via event
1350
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
1351
-			new ServiceRegistration('test', SuccessfulSyncProvider::class)
1352
-		]);
1353
-		$this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([]);
1354
-		$this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([]);
1355
-		$this->registrationContext->expects($this->any())->method('getSpeechToTextProviders')->willReturn([]);
1356
-
1357
-		$conflictingExternalProvider = new ConflictingExternalProvider(); // ID = 'test:sync:success'
1358
-		$this->configureEventDispatcherMock(providersToAdd: [$conflictingExternalProvider]);
1359
-		$this->manager = $this->createManagerInstance();
1360
-
1361
-		// Act
1362
-		$providers = $this->manager->getProviders();
1363
-
1364
-		// Assert: Only the local provider should be present for the conflicting ID
1365
-		self::assertArrayHasKey(SuccessfulSyncProvider::ID, $providers);
1366
-		self::assertInstanceOf(SuccessfulSyncProvider::class, $providers[SuccessfulSyncProvider::ID]);
1367
-		self::assertCount(1, $providers); // Ensure no extra provider was added
1368
-	}
1369
-
1370
-	public function testTriggerableProviderWithNoOtherRunningTasks() {
1371
-		// Arrange: Local provider registered, conflicting external provider via event
1372
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([]);
1373
-		$this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([]);
1374
-		$this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([]);
1375
-		$this->registrationContext->expects($this->any())->method('getSpeechToTextProviders')->willReturn([]);
1376
-
1377
-		$externalProvider = $this->createPartialMock(ExternalTriggerableProvider::class, ['trigger']);
1378
-		$externalProvider->expects($this->once())->method('trigger');
1379
-		$this->configureEventDispatcherMock(providersToAdd: [$externalProvider]);
1380
-		$this->manager = $this->createManagerInstance();
1381
-
1382
-		// Act
1383
-		$task = new Task($externalProvider->getTaskTypeId(), ['input' => ''], 'tests', null);
1384
-		$this->manager->scheduleTask($task);
1385
-	}
1386
-
1387
-	public function testTriggerableProviderWithOtherRunningTasks() {
1388
-		// Arrange: Local provider registered, conflicting external provider via event
1389
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([]);
1390
-		$this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([]);
1391
-		$this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([]);
1392
-		$this->registrationContext->expects($this->any())->method('getSpeechToTextProviders')->willReturn([]);
1393
-
1394
-		$externalProvider = $this->createPartialMock(ExternalTriggerableProvider::class, ['trigger']);
1395
-		$externalProvider->expects($this->once())->method('trigger');
1396
-		$this->configureEventDispatcherMock(providersToAdd: [$externalProvider]);
1397
-		$this->manager = $this->createManagerInstance();
1398
-
1399
-		$task = new Task($externalProvider->getTaskTypeId(), ['input' => ''], 'tests', null);
1400
-		$this->manager->scheduleTask($task);
1401
-		$this->manager->lockTask($task);
1402
-
1403
-		// Act
1404
-		$task = new Task($externalProvider->getTaskTypeId(), ['input' => ''], 'tests', null);
1405
-		$this->manager->scheduleTask($task);
1406
-	}
1407
-
1408
-	public function testMergeTaskTypesLocalAndEvent() {
1409
-		// Arrange: Local type registered, DIFFERENT external type via event
1410
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
1411
-			new ServiceRegistration('test', AsyncProvider::class)
1412
-		]);
1413
-		$this->registrationContext->expects($this->any())->method('getTaskProcessingTaskTypes')->willReturn([
1414
-			new ServiceRegistration('test', AudioToImage::class)
1415
-		]);
1416
-		$this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([]);
1417
-		$this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([]);
1418
-		$this->registrationContext->expects($this->any())->method('getSpeechToTextProviders')->willReturn([]);
1419
-
1420
-		$externalTaskType = new ExternalTaskType(); // ID = 'event:external:tasktype'
1421
-		$externalProvider = new ExternalProvider(); // Handles 'event:external:tasktype'
1422
-		$this->configureEventDispatcherMock(
1423
-			providersToAdd: [$externalProvider],
1424
-			taskTypesToAdd: [$externalTaskType]
1425
-		);
1426
-		$this->manager = $this->createManagerInstance();
1427
-
1428
-		// Act
1429
-		$availableTypes = $this->manager->getAvailableTaskTypes();
1430
-		$availableTypeIds = $this->manager->getAvailableTaskTypeIds();
1431
-
1432
-		// Assert: Both task types should be available
1433
-		self::assertContains(AudioToImage::ID, $availableTypeIds);
1434
-		self::assertArrayHasKey(AudioToImage::ID, $availableTypes);
1435
-		self::assertEquals(AudioToImage::class, $availableTypes[AudioToImage::ID]['name']);
1436
-
1437
-		self::assertContains(ExternalTaskType::ID, $availableTypeIds);
1438
-		self::assertArrayHasKey(ExternalTaskType::ID, $availableTypes);
1439
-		self::assertEquals('External Task Type via Event', $availableTypes[ExternalTaskType::ID]['name']);
1440
-
1441
-		self::assertCount(2, $availableTypes);
1442
-	}
1443
-
1444
-	private function createManagerInstance(): Manager {
1445
-		// Clear potentially cached config values if needed
1446
-		$this->appConfig->deleteKey('core', 'ai.taskprocessing_type_preferences');
1447
-
1448
-		// Re-create Text2ImageManager if its state matters or mocks change
1449
-		$text2imageManager = new \OC\TextToImage\Manager(
1450
-			$this->serverContainer,
1451
-			$this->coordinator,
1452
-			Server::get(LoggerInterface::class),
1453
-			$this->jobList,
1454
-			Server::get(\OC\TextToImage\Db\TaskMapper::class),
1455
-			$this->config, // Use the shared config mock
1456
-			Server::get(IAppDataFactory::class),
1457
-		);
1458
-
1459
-		return new Manager(
1460
-			$this->appConfig,
1461
-			$this->coordinator,
1462
-			$this->serverContainer,
1463
-			Server::get(LoggerInterface::class),
1464
-			$this->taskMapper,
1465
-			$this->jobList,
1466
-			$this->eventDispatcher, // Use the potentially reconfigured mock
1467
-			Server::get(IAppDataFactory::class),
1468
-			$this->rootFolder,
1469
-			$text2imageManager,
1470
-			$this->userMountCache,
1471
-			Server::get(IClientService::class),
1472
-			Server::get(IAppManager::class),
1473
-			Server::get(IUserManager::class),
1474
-			Server::get(IUserSession::class),
1475
-			Server::get(ICacheFactory::class),
1476
-			Server::get(IFactory::class),
1477
-		);
1478
-	}
1479
-
1480
-	private function configureEventDispatcherMock(
1481
-		array $providersToAdd = [],
1482
-		array $taskTypesToAdd = [],
1483
-		?int $expectedCalls = null,
1484
-	): void {
1485
-		$dispatchExpectation = $expectedCalls === null ? $this->any() : $this->exactly($expectedCalls);
1486
-
1487
-		$this->eventDispatcher->expects($dispatchExpectation)
1488
-			->method('dispatchTyped')
1489
-			->willReturnCallback(function (object $event) use ($providersToAdd, $taskTypesToAdd): void {
1490
-				if ($event instanceof GetTaskProcessingProvidersEvent) {
1491
-					foreach ($providersToAdd as $providerInstance) {
1492
-						$event->addProvider($providerInstance);
1493
-					}
1494
-					foreach ($taskTypesToAdd as $taskTypeInstance) {
1495
-						$event->addTaskType($taskTypeInstance);
1496
-					}
1497
-				}
1498
-			});
1499
-	}
637
+    private IManager $manager;
638
+    private Coordinator $coordinator;
639
+    private array $providers;
640
+    private IServerContainer $serverContainer;
641
+    private IEventDispatcher $eventDispatcher;
642
+    private RegistrationContext $registrationContext;
643
+    private TaskMapper $taskMapper;
644
+    private IJobList $jobList;
645
+    private IUserMountCache $userMountCache;
646
+    private IRootFolder $rootFolder;
647
+    private IConfig $config;
648
+    private IAppConfig $appConfig;
649
+
650
+    public const TEST_USER = 'testuser';
651
+
652
+    protected function setUp(): void {
653
+        parent::setUp();
654
+
655
+        $this->providers = [
656
+            SuccessfulSyncProvider::class => new SuccessfulSyncProvider(),
657
+            FailingSyncProvider::class => new FailingSyncProvider(),
658
+            FailingSyncProviderWithUserFacingError::class => new FailingSyncProviderWithUserFacingError(),
659
+            BrokenSyncProvider::class => new BrokenSyncProvider(),
660
+            AsyncProvider::class => new AsyncProvider(),
661
+            AudioToImage::class => new AudioToImage(),
662
+            SuccessfulTextProcessingSummaryProvider::class => new SuccessfulTextProcessingSummaryProvider(),
663
+            FailingTextProcessingSummaryProvider::class => new FailingTextProcessingSummaryProvider(),
664
+            SuccessfulTextToImageProvider::class => new SuccessfulTextToImageProvider(),
665
+            FailingTextToImageProvider::class => new FailingTextToImageProvider(),
666
+            ExternalProvider::class => new ExternalProvider(),
667
+            ExternalTriggerableProvider::class => new ExternalTriggerableProvider(),
668
+            ConflictingExternalProvider::class => new ConflictingExternalProvider(),
669
+            ExternalTaskType::class => new ExternalTaskType(),
670
+            ConflictingExternalTaskType::class => new ConflictingExternalTaskType(),
671
+        ];
672
+
673
+        $userManager = Server::get(IUserManager::class);
674
+        if (!$userManager->userExists(self::TEST_USER)) {
675
+            $userManager->createUser(self::TEST_USER, 'test');
676
+        }
677
+
678
+        $this->serverContainer = $this->createMock(IServerContainer::class);
679
+        $this->serverContainer->expects($this->any())->method('get')->willReturnCallback(function ($class) {
680
+            return $this->providers[$class];
681
+        });
682
+
683
+        $this->eventDispatcher = new EventDispatcher(
684
+            new \Symfony\Component\EventDispatcher\EventDispatcher(),
685
+            $this->serverContainer,
686
+            Server::get(LoggerInterface::class),
687
+        );
688
+
689
+        $this->registrationContext = $this->createMock(RegistrationContext::class);
690
+        $this->coordinator = $this->createMock(Coordinator::class);
691
+        $this->coordinator->expects($this->any())->method('getRegistrationContext')->willReturn($this->registrationContext);
692
+
693
+        $this->rootFolder = Server::get(IRootFolder::class);
694
+
695
+        $this->taskMapper = Server::get(TaskMapper::class);
696
+
697
+        $this->jobList = $this->createPartialMock(DummyJobList::class, ['add']);
698
+        $this->jobList->expects($this->any())->method('add')->willReturnCallback(function (): void {
699
+        });
700
+
701
+        $this->eventDispatcher = $this->createMock(IEventDispatcher::class);
702
+        $this->configureEventDispatcherMock();
703
+
704
+        $text2imageManager = new \OC\TextToImage\Manager(
705
+            $this->serverContainer,
706
+            $this->coordinator,
707
+            Server::get(LoggerInterface::class),
708
+            $this->jobList,
709
+            Server::get(\OC\TextToImage\Db\TaskMapper::class),
710
+            Server::get(IConfig::class),
711
+            Server::get(IAppDataFactory::class),
712
+        );
713
+
714
+        $this->userMountCache = $this->createMock(IUserMountCache::class);
715
+        $this->config = Server::get(IConfig::class);
716
+        $this->appConfig = Server::get(IAppConfig::class);
717
+        $this->manager = new Manager(
718
+            $this->appConfig,
719
+            $this->coordinator,
720
+            $this->serverContainer,
721
+            Server::get(LoggerInterface::class),
722
+            $this->taskMapper,
723
+            $this->jobList,
724
+            $this->eventDispatcher,
725
+            Server::get(IAppDataFactory::class),
726
+            Server::get(IRootFolder::class),
727
+            $text2imageManager,
728
+            $this->userMountCache,
729
+            Server::get(IClientService::class),
730
+            Server::get(IAppManager::class),
731
+            $userManager,
732
+            Server::get(IUserSession::class),
733
+            Server::get(ICacheFactory::class),
734
+            Server::get(IFactory::class),
735
+        );
736
+    }
737
+
738
+    private function getFile(string $name, string $content): File {
739
+        $folder = $this->rootFolder->getUserFolder(self::TEST_USER);
740
+        $file = $folder->newFile($name, $content);
741
+        return $file;
742
+    }
743
+
744
+    public function testShouldNotHaveAnyProviders(): void {
745
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([]);
746
+        self::assertCount(0, $this->manager->getAvailableTaskTypes());
747
+        self::assertCount(0, $this->manager->getAvailableTaskTypeIds());
748
+        self::assertFalse($this->manager->hasProviders());
749
+        self::expectException(PreConditionNotMetException::class);
750
+        $this->manager->scheduleTask(new Task(TextToText::ID, ['input' => 'Hello'], 'test', null));
751
+    }
752
+
753
+    public function testProviderShouldBeRegisteredAndTaskTypeDisabled(): void {
754
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
755
+            new ServiceRegistration('test', SuccessfulSyncProvider::class)
756
+        ]);
757
+        $taskProcessingTypeSettings = [
758
+            TextToText::ID => false,
759
+        ];
760
+        $this->appConfig->setValueString('core', 'ai.taskprocessing_type_preferences', json_encode($taskProcessingTypeSettings), lazy: true);
761
+        self::assertCount(0, $this->manager->getAvailableTaskTypes());
762
+        self::assertCount(1, $this->manager->getAvailableTaskTypes(true));
763
+        self::assertCount(0, $this->manager->getAvailableTaskTypeIds());
764
+        self::assertCount(1, $this->manager->getAvailableTaskTypeIds(true));
765
+        self::assertTrue($this->manager->hasProviders());
766
+        self::expectException(PreConditionNotMetException::class);
767
+        $this->manager->scheduleTask(new Task(TextToText::ID, ['input' => 'Hello'], 'test', null));
768
+    }
769
+
770
+
771
+    public function testProviderShouldBeRegisteredAndTaskFailValidation(): void {
772
+        $this->appConfig->setValueString('core', 'ai.taskprocessing_type_preferences', '', lazy: true);
773
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
774
+            new ServiceRegistration('test', BrokenSyncProvider::class)
775
+        ]);
776
+        self::assertCount(1, $this->manager->getAvailableTaskTypes());
777
+        self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
778
+        self::assertTrue($this->manager->hasProviders());
779
+        $task = new Task(TextToText::ID, ['wrongInputKey' => 'Hello'], 'test', null);
780
+        self::assertNull($task->getId());
781
+        self::expectException(ValidationException::class);
782
+        $this->manager->scheduleTask($task);
783
+    }
784
+
785
+    public function testProviderShouldBeRegisteredAndTaskWithFilesFailValidation(): void {
786
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingTaskTypes')->willReturn([
787
+            new ServiceRegistration('test', AudioToImage::class)
788
+        ]);
789
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
790
+            new ServiceRegistration('test', AsyncProvider::class)
791
+        ]);
792
+        $user = $this->createMock(IUser::class);
793
+        $user->expects($this->any())->method('getUID')->willReturn(null);
794
+        $mount = $this->createMock(ICachedMountInfo::class);
795
+        $mount->expects($this->any())->method('getUser')->willReturn($user);
796
+        $this->userMountCache->expects($this->any())->method('getMountsForFileId')->willReturn([$mount]);
797
+
798
+        self::assertCount(1, $this->manager->getAvailableTaskTypes());
799
+        self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
800
+        self::assertTrue($this->manager->hasProviders());
801
+
802
+        $audioId = $this->getFile('audioInput', 'Hello')->getId();
803
+        $task = new Task(AudioToImage::ID, ['audio' => $audioId], 'test', null);
804
+        self::assertNull($task->getId());
805
+        self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus());
806
+        self::expectException(UnauthorizedException::class);
807
+        $this->manager->scheduleTask($task);
808
+    }
809
+
810
+    public function testProviderShouldBeRegisteredAndFail(): void {
811
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
812
+            new ServiceRegistration('test', FailingSyncProvider::class)
813
+        ]);
814
+        self::assertCount(1, $this->manager->getAvailableTaskTypes());
815
+        self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
816
+        self::assertTrue($this->manager->hasProviders());
817
+        $task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null);
818
+        self::assertNull($task->getId());
819
+        self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus());
820
+        $this->manager->scheduleTask($task);
821
+        self::assertNotNull($task->getId());
822
+        self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus());
823
+
824
+        $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskFailedEvent::class));
825
+
826
+        $backgroundJob = new SynchronousBackgroundJob(
827
+            Server::get(ITimeFactory::class),
828
+            $this->manager,
829
+            $this->jobList,
830
+            Server::get(LoggerInterface::class),
831
+        );
832
+        $backgroundJob->start($this->jobList);
833
+
834
+        $task = $this->manager->getTask($task->getId());
835
+        self::assertEquals(Task::STATUS_FAILED, $task->getStatus());
836
+        self::assertEquals(FailingSyncProvider::ERROR_MESSAGE, $task->getErrorMessage());
837
+    }
838
+
839
+    public function testProviderShouldBeRegisteredAndFailWithUserFacingMessage(): void {
840
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
841
+            new ServiceRegistration('test', FailingSyncProviderWithUserFacingError::class)
842
+        ]);
843
+        self::assertCount(1, $this->manager->getAvailableTaskTypes());
844
+        self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
845
+        self::assertTrue($this->manager->hasProviders());
846
+        $task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null);
847
+        self::assertNull($task->getId());
848
+        self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus());
849
+        $this->manager->scheduleTask($task);
850
+        self::assertNotNull($task->getId());
851
+        self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus());
852
+
853
+        $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskFailedEvent::class));
854
+
855
+        $backgroundJob = new SynchronousBackgroundJob(
856
+            Server::get(ITimeFactory::class),
857
+            $this->manager,
858
+            $this->jobList,
859
+            Server::get(LoggerInterface::class),
860
+        );
861
+        $backgroundJob->start($this->jobList);
862
+
863
+        $task = $this->manager->getTask($task->getId());
864
+        self::assertEquals(Task::STATUS_FAILED, $task->getStatus());
865
+        self::assertEquals(FailingSyncProviderWithUserFacingError::ERROR_MESSAGE, $task->getErrorMessage());
866
+        self::assertEquals(FailingSyncProviderWithUserFacingError::USER_FACING_ERROR_MESSAGE, $task->getUserFacingErrorMessage());
867
+    }
868
+
869
+    public function testProviderShouldBeRegisteredAndFailOutputValidation(): void {
870
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
871
+            new ServiceRegistration('test', BrokenSyncProvider::class)
872
+        ]);
873
+        self::assertCount(1, $this->manager->getAvailableTaskTypes());
874
+        self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
875
+        self::assertTrue($this->manager->hasProviders());
876
+        $task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null);
877
+        self::assertNull($task->getId());
878
+        self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus());
879
+        $this->manager->scheduleTask($task);
880
+        self::assertNotNull($task->getId());
881
+        self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus());
882
+
883
+        $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskFailedEvent::class));
884
+
885
+        $backgroundJob = new SynchronousBackgroundJob(
886
+            Server::get(ITimeFactory::class),
887
+            $this->manager,
888
+            $this->jobList,
889
+            Server::get(LoggerInterface::class),
890
+        );
891
+        $backgroundJob->start($this->jobList);
892
+
893
+        $task = $this->manager->getTask($task->getId());
894
+        self::assertEquals(Task::STATUS_FAILED, $task->getStatus());
895
+        self::assertEquals('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', $task->getErrorMessage());
896
+    }
897
+
898
+    public function testProviderShouldBeRegisteredAndRun(): void {
899
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
900
+            new ServiceRegistration('test', SuccessfulSyncProvider::class)
901
+        ]);
902
+        self::assertCount(1, $this->manager->getAvailableTaskTypes());
903
+        self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
904
+        $taskTypeStruct = $this->manager->getAvailableTaskTypes()[array_keys($this->manager->getAvailableTaskTypes())[0]];
905
+        self::assertTrue(isset($taskTypeStruct['inputShape']['input']));
906
+        self::assertEquals(EShapeType::Text, $taskTypeStruct['inputShape']['input']->getShapeType());
907
+        self::assertTrue(isset($taskTypeStruct['optionalInputShape']['optionalKey']));
908
+        self::assertEquals(EShapeType::Text, $taskTypeStruct['optionalInputShape']['optionalKey']->getShapeType());
909
+        self::assertTrue(isset($taskTypeStruct['outputShape']['output']));
910
+        self::assertEquals(EShapeType::Text, $taskTypeStruct['outputShape']['output']->getShapeType());
911
+        self::assertTrue(isset($taskTypeStruct['optionalOutputShape']['optionalKey']));
912
+        self::assertEquals(EShapeType::Text, $taskTypeStruct['optionalOutputShape']['optionalKey']->getShapeType());
913
+
914
+        self::assertTrue($this->manager->hasProviders());
915
+        $task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null);
916
+        self::assertNull($task->getId());
917
+        self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus());
918
+        $this->manager->scheduleTask($task);
919
+        self::assertNotNull($task->getId());
920
+        self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus());
921
+
922
+        // Task object retrieved from db is up-to-date
923
+        $task2 = $this->manager->getTask($task->getId());
924
+        self::assertEquals($task->getId(), $task2->getId());
925
+        self::assertEquals(['input' => 'Hello'], $task2->getInput());
926
+        self::assertNull($task2->getOutput());
927
+        self::assertEquals(Task::STATUS_SCHEDULED, $task2->getStatus());
928
+
929
+        $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class));
930
+
931
+        $backgroundJob = new SynchronousBackgroundJob(
932
+            Server::get(ITimeFactory::class),
933
+            $this->manager,
934
+            $this->jobList,
935
+            Server::get(LoggerInterface::class),
936
+        );
937
+        $backgroundJob->start($this->jobList);
938
+
939
+        $task = $this->manager->getTask($task->getId());
940
+        self::assertEquals(Task::STATUS_SUCCESSFUL, $task->getStatus(), 'Status is ' . $task->getStatus() . ' with error message: ' . $task->getErrorMessage());
941
+        self::assertEquals(['output' => 'Hello'], $task->getOutput());
942
+        self::assertEquals(1, $task->getProgress());
943
+    }
944
+
945
+    public function testTaskTypeExplicitlyEnabled(): void {
946
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
947
+            new ServiceRegistration('test', SuccessfulSyncProvider::class)
948
+        ]);
949
+
950
+        $taskProcessingTypeSettings = [
951
+            TextToText::ID => true,
952
+        ];
953
+        $this->appConfig->setValueString('core', 'ai.taskprocessing_type_preferences', json_encode($taskProcessingTypeSettings), lazy: true);
954
+
955
+        self::assertCount(1, $this->manager->getAvailableTaskTypes());
956
+        self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
957
+
958
+        self::assertTrue($this->manager->hasProviders());
959
+        $task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null);
960
+        self::assertNull($task->getId());
961
+        self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus());
962
+        $this->manager->scheduleTask($task);
963
+        self::assertNotNull($task->getId());
964
+        self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus());
965
+
966
+        $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class));
967
+
968
+        $backgroundJob = new SynchronousBackgroundJob(
969
+            Server::get(ITimeFactory::class),
970
+            $this->manager,
971
+            $this->jobList,
972
+            Server::get(LoggerInterface::class),
973
+        );
974
+        $backgroundJob->start($this->jobList);
975
+
976
+        $task = $this->manager->getTask($task->getId());
977
+        self::assertEquals(Task::STATUS_SUCCESSFUL, $task->getStatus(), 'Status is ' . $task->getStatus() . ' with error message: ' . $task->getErrorMessage());
978
+        self::assertEquals(['output' => 'Hello'], $task->getOutput());
979
+        self::assertEquals(1, $task->getProgress());
980
+    }
981
+
982
+    public function testAsyncProviderWithFilesShouldBeRegisteredAndRunReturningRawFileData(): void {
983
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingTaskTypes')->willReturn([
984
+            new ServiceRegistration('test', AudioToImage::class)
985
+        ]);
986
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
987
+            new ServiceRegistration('test', AsyncProvider::class)
988
+        ]);
989
+
990
+        $user = $this->createMock(IUser::class);
991
+        $user->expects($this->any())->method('getUID')->willReturn('testuser');
992
+        $mount = $this->createMock(ICachedMountInfo::class);
993
+        $mount->expects($this->any())->method('getUser')->willReturn($user);
994
+        $this->userMountCache->expects($this->any())->method('getMountsForFileId')->willReturn([$mount]);
995
+
996
+        self::assertCount(1, $this->manager->getAvailableTaskTypes());
997
+        self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
998
+
999
+        self::assertTrue($this->manager->hasProviders());
1000
+        $audioId = $this->getFile('audioInput', 'Hello')->getId();
1001
+        $task = new Task(AudioToImage::ID, ['audio' => $audioId], 'test', 'testuser');
1002
+        self::assertNull($task->getId());
1003
+        self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus());
1004
+        $this->manager->scheduleTask($task);
1005
+        self::assertNotNull($task->getId());
1006
+        self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus());
1007
+
1008
+        // Task object retrieved from db is up-to-date
1009
+        $task2 = $this->manager->getTask($task->getId());
1010
+        self::assertEquals($task->getId(), $task2->getId());
1011
+        self::assertEquals(['audio' => $audioId], $task2->getInput());
1012
+        self::assertNull($task2->getOutput());
1013
+        self::assertEquals(Task::STATUS_SCHEDULED, $task2->getStatus());
1014
+
1015
+        $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class));
1016
+
1017
+        $this->manager->setTaskProgress($task2->getId(), 0.1);
1018
+        $input = $this->manager->prepareInputData($task2);
1019
+        self::assertTrue(isset($input['audio']));
1020
+        self::assertInstanceOf(File::class, $input['audio']);
1021
+        self::assertEquals($audioId, $input['audio']->getId());
1022
+
1023
+        $this->manager->setTaskResult($task2->getId(), null, ['spectrogram' => 'World']);
1024
+
1025
+        $task = $this->manager->getTask($task->getId());
1026
+        self::assertEquals(Task::STATUS_SUCCESSFUL, $task->getStatus());
1027
+        self::assertEquals(1, $task->getProgress());
1028
+        self::assertTrue(isset($task->getOutput()['spectrogram']));
1029
+        $node = $this->rootFolder->getFirstNodeByIdInPath($task->getOutput()['spectrogram'], '/' . $this->rootFolder->getAppDataDirectoryName() . '/');
1030
+        self::assertNotNull($node);
1031
+        self::assertInstanceOf(File::class, $node);
1032
+        self::assertEquals('World', $node->getContent());
1033
+    }
1034
+
1035
+    public function testAsyncProviderWithFilesShouldBeRegisteredAndRunReturningFileIds(): void {
1036
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingTaskTypes')->willReturn([
1037
+            new ServiceRegistration('test', AudioToImage::class)
1038
+        ]);
1039
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
1040
+            new ServiceRegistration('test', AsyncProvider::class)
1041
+        ]);
1042
+        $user = $this->createMock(IUser::class);
1043
+        $user->expects($this->any())->method('getUID')->willReturn('testuser');
1044
+        $mount = $this->createMock(ICachedMountInfo::class);
1045
+        $mount->expects($this->any())->method('getUser')->willReturn($user);
1046
+        $this->userMountCache->expects($this->any())->method('getMountsForFileId')->willReturn([$mount]);
1047
+        self::assertCount(1, $this->manager->getAvailableTaskTypes());
1048
+        self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
1049
+
1050
+        self::assertTrue($this->manager->hasProviders());
1051
+        $audioId = $this->getFile('audioInput', 'Hello')->getId();
1052
+        $task = new Task(AudioToImage::ID, ['audio' => $audioId], 'test', 'testuser');
1053
+        self::assertNull($task->getId());
1054
+        self::assertEquals(Task::STATUS_UNKNOWN, $task->getStatus());
1055
+        $this->manager->scheduleTask($task);
1056
+        self::assertNotNull($task->getId());
1057
+        self::assertEquals(Task::STATUS_SCHEDULED, $task->getStatus());
1058
+
1059
+        // Task object retrieved from db is up-to-date
1060
+        $task2 = $this->manager->getTask($task->getId());
1061
+        self::assertEquals($task->getId(), $task2->getId());
1062
+        self::assertEquals(['audio' => $audioId], $task2->getInput());
1063
+        self::assertNull($task2->getOutput());
1064
+        self::assertEquals(Task::STATUS_SCHEDULED, $task2->getStatus());
1065
+
1066
+        $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class));
1067
+
1068
+        $this->manager->setTaskProgress($task2->getId(), 0.1);
1069
+        $input = $this->manager->prepareInputData($task2);
1070
+        self::assertTrue(isset($input['audio']));
1071
+        self::assertInstanceOf(File::class, $input['audio']);
1072
+        self::assertEquals($audioId, $input['audio']->getId());
1073
+
1074
+        $outputFileId = $this->getFile('audioOutput', 'World')->getId();
1075
+
1076
+        $this->manager->setTaskResult($task2->getId(), null, ['spectrogram' => $outputFileId], true);
1077
+
1078
+        $task = $this->manager->getTask($task->getId());
1079
+        self::assertEquals(Task::STATUS_SUCCESSFUL, $task->getStatus());
1080
+        self::assertEquals(1, $task->getProgress());
1081
+        self::assertTrue(isset($task->getOutput()['spectrogram']));
1082
+        $node = $this->rootFolder->getFirstNodeById($task->getOutput()['spectrogram']);
1083
+        self::assertNotNull($node, 'fileId:' . $task->getOutput()['spectrogram']);
1084
+        self::assertInstanceOf(File::class, $node);
1085
+        self::assertEquals('World', $node->getContent());
1086
+    }
1087
+
1088
+    public function testNonexistentTask(): void {
1089
+        $this->expectException(NotFoundException::class);
1090
+        $this->manager->getTask(2147483646);
1091
+    }
1092
+
1093
+    public function testOldTasksShouldBeCleanedUp(): void {
1094
+        $currentTime = new \DateTime('now');
1095
+        $timeFactory = $this->createMock(ITimeFactory::class);
1096
+        $timeFactory->expects($this->any())->method('getDateTime')->willReturnCallback(fn () => $currentTime);
1097
+        $timeFactory->expects($this->any())->method('getTime')->willReturnCallback(fn () => $currentTime->getTimestamp());
1098
+
1099
+        $this->taskMapper = new TaskMapper(
1100
+            Server::get(IDBConnection::class),
1101
+            $timeFactory,
1102
+        );
1103
+
1104
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
1105
+            new ServiceRegistration('test', SuccessfulSyncProvider::class)
1106
+        ]);
1107
+        self::assertCount(1, $this->manager->getAvailableTaskTypes());
1108
+        self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
1109
+        self::assertTrue($this->manager->hasProviders());
1110
+        $task = new Task(TextToText::ID, ['input' => 'Hello'], 'test', null);
1111
+        $this->manager->scheduleTask($task);
1112
+
1113
+        $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class));
1114
+
1115
+        $backgroundJob = new SynchronousBackgroundJob(
1116
+            Server::get(ITimeFactory::class),
1117
+            $this->manager,
1118
+            $this->jobList,
1119
+            Server::get(LoggerInterface::class),
1120
+        );
1121
+        $backgroundJob->start($this->jobList);
1122
+
1123
+        $task = $this->manager->getTask($task->getId());
1124
+
1125
+        $currentTime = $currentTime->add(new \DateInterval('P1Y'));
1126
+        // run background job
1127
+        $bgJob = new RemoveOldTasksBackgroundJob(
1128
+            $timeFactory,
1129
+            $this->manager,
1130
+            $this->taskMapper,
1131
+            Server::get(LoggerInterface::class),
1132
+            Server::get(IAppDataFactory::class),
1133
+        );
1134
+        $bgJob->setArgument([]);
1135
+        $bgJob->start($this->jobList);
1136
+
1137
+        $this->expectException(NotFoundException::class);
1138
+        $this->manager->getTask($task->getId());
1139
+    }
1140
+
1141
+    public function testShouldTransparentlyHandleTextProcessingProviders(): void {
1142
+        $this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([
1143
+            new ServiceRegistration('test', SuccessfulTextProcessingSummaryProvider::class)
1144
+        ]);
1145
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
1146
+        ]);
1147
+        $taskTypes = $this->manager->getAvailableTaskTypes();
1148
+        self::assertCount(1, $taskTypes);
1149
+        self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
1150
+        self::assertTrue(isset($taskTypes[TextToTextSummary::ID]));
1151
+        self::assertTrue($this->manager->hasProviders());
1152
+        $task = new Task(TextToTextSummary::ID, ['input' => 'Hello'], 'test', null);
1153
+        $this->manager->scheduleTask($task);
1154
+
1155
+        $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class));
1156
+
1157
+        $backgroundJob = new SynchronousBackgroundJob(
1158
+            Server::get(ITimeFactory::class),
1159
+            $this->manager,
1160
+            $this->jobList,
1161
+            Server::get(LoggerInterface::class),
1162
+        );
1163
+        $backgroundJob->start($this->jobList);
1164
+
1165
+        $task = $this->manager->getTask($task->getId());
1166
+        self::assertEquals(Task::STATUS_SUCCESSFUL, $task->getStatus());
1167
+        self::assertIsArray($task->getOutput());
1168
+        self::assertTrue(isset($task->getOutput()['output']));
1169
+        self::assertEquals('Hello Summarize', $task->getOutput()['output']);
1170
+        self::assertTrue($this->providers[SuccessfulTextProcessingSummaryProvider::class]->ran);
1171
+    }
1172
+
1173
+    public function testShouldTransparentlyHandleFailingTextProcessingProviders(): void {
1174
+        $this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([
1175
+            new ServiceRegistration('test', FailingTextProcessingSummaryProvider::class)
1176
+        ]);
1177
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
1178
+        ]);
1179
+        $taskTypes = $this->manager->getAvailableTaskTypes();
1180
+        self::assertCount(1, $taskTypes);
1181
+        self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
1182
+        self::assertTrue(isset($taskTypes[TextToTextSummary::ID]));
1183
+        self::assertTrue($this->manager->hasProviders());
1184
+        $task = new Task(TextToTextSummary::ID, ['input' => 'Hello'], 'test', null);
1185
+        $this->manager->scheduleTask($task);
1186
+
1187
+        $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskFailedEvent::class));
1188
+
1189
+        $backgroundJob = new SynchronousBackgroundJob(
1190
+            Server::get(ITimeFactory::class),
1191
+            $this->manager,
1192
+            $this->jobList,
1193
+            Server::get(LoggerInterface::class),
1194
+        );
1195
+        $backgroundJob->start($this->jobList);
1196
+
1197
+        $task = $this->manager->getTask($task->getId());
1198
+        self::assertEquals(Task::STATUS_FAILED, $task->getStatus());
1199
+        self::assertTrue($task->getOutput() === null);
1200
+        self::assertEquals('ERROR', $task->getErrorMessage());
1201
+        self::assertTrue($this->providers[FailingTextProcessingSummaryProvider::class]->ran);
1202
+    }
1203
+
1204
+    public function testShouldTransparentlyHandleText2ImageProviders(): void {
1205
+        $this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([
1206
+            new ServiceRegistration('test', SuccessfulTextToImageProvider::class)
1207
+        ]);
1208
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
1209
+        ]);
1210
+        $taskTypes = $this->manager->getAvailableTaskTypes();
1211
+        self::assertCount(1, $taskTypes);
1212
+        self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
1213
+        self::assertTrue(isset($taskTypes[TextToImage::ID]));
1214
+        self::assertTrue($this->manager->hasProviders());
1215
+        $task = new Task(TextToImage::ID, ['input' => 'Hello', 'numberOfImages' => 3], 'test', null);
1216
+        $this->manager->scheduleTask($task);
1217
+
1218
+        $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskSuccessfulEvent::class));
1219
+
1220
+        $backgroundJob = new SynchronousBackgroundJob(
1221
+            Server::get(ITimeFactory::class),
1222
+            $this->manager,
1223
+            $this->jobList,
1224
+            Server::get(LoggerInterface::class),
1225
+        );
1226
+        $backgroundJob->start($this->jobList);
1227
+
1228
+        $task = $this->manager->getTask($task->getId());
1229
+        self::assertEquals(Task::STATUS_SUCCESSFUL, $task->getStatus());
1230
+        self::assertIsArray($task->getOutput());
1231
+        self::assertTrue(isset($task->getOutput()['images']));
1232
+        self::assertIsArray($task->getOutput()['images']);
1233
+        self::assertCount(3, $task->getOutput()['images']);
1234
+        self::assertTrue($this->providers[SuccessfulTextToImageProvider::class]->ran);
1235
+        $node = $this->rootFolder->getFirstNodeByIdInPath($task->getOutput()['images'][0], '/' . $this->rootFolder->getAppDataDirectoryName() . '/');
1236
+        self::assertNotNull($node);
1237
+        self::assertInstanceOf(File::class, $node);
1238
+        self::assertEquals('test', $node->getContent());
1239
+    }
1240
+
1241
+    public function testShouldTransparentlyHandleFailingText2ImageProviders(): void {
1242
+        $this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([
1243
+            new ServiceRegistration('test', FailingTextToImageProvider::class)
1244
+        ]);
1245
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
1246
+        ]);
1247
+        $taskTypes = $this->manager->getAvailableTaskTypes();
1248
+        self::assertCount(1, $taskTypes);
1249
+        self::assertCount(1, $this->manager->getAvailableTaskTypeIds());
1250
+        self::assertTrue(isset($taskTypes[TextToImage::ID]));
1251
+        self::assertTrue($this->manager->hasProviders());
1252
+        $task = new Task(TextToImage::ID, ['input' => 'Hello', 'numberOfImages' => 3], 'test', null);
1253
+        $this->manager->scheduleTask($task);
1254
+
1255
+        $this->eventDispatcher->expects($this->once())->method('dispatchTyped')->with(new IsInstanceOf(TaskFailedEvent::class));
1256
+
1257
+        $backgroundJob = new SynchronousBackgroundJob(
1258
+            Server::get(ITimeFactory::class),
1259
+            $this->manager,
1260
+            $this->jobList,
1261
+            Server::get(LoggerInterface::class),
1262
+        );
1263
+        $backgroundJob->start($this->jobList);
1264
+
1265
+        $task = $this->manager->getTask($task->getId());
1266
+        self::assertEquals(Task::STATUS_FAILED, $task->getStatus());
1267
+        self::assertTrue($task->getOutput() === null);
1268
+        self::assertEquals('ERROR', $task->getErrorMessage());
1269
+        self::assertTrue($this->providers[FailingTextToImageProvider::class]->ran);
1270
+    }
1271
+
1272
+    public function testMergeProvidersLocalAndEvent() {
1273
+        // Arrange: Local provider registered, DIFFERENT external provider via event
1274
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
1275
+            new ServiceRegistration('test', SuccessfulSyncProvider::class)
1276
+        ]);
1277
+        $this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([]);
1278
+        $this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([]);
1279
+        $this->registrationContext->expects($this->any())->method('getSpeechToTextProviders')->willReturn([]);
1280
+
1281
+        $externalProvider = new ExternalProvider(); // ID = 'event:external:provider'
1282
+        $this->configureEventDispatcherMock(providersToAdd: [$externalProvider]);
1283
+        $this->manager = $this->createManagerInstance();
1284
+
1285
+        // Act
1286
+        $providers = $this->manager->getProviders();
1287
+
1288
+        // Assert: Both providers should be present
1289
+        self::assertArrayHasKey(SuccessfulSyncProvider::ID, $providers);
1290
+        self::assertInstanceOf(SuccessfulSyncProvider::class, $providers[SuccessfulSyncProvider::ID]);
1291
+        self::assertArrayHasKey(ExternalProvider::ID, $providers);
1292
+        self::assertInstanceOf(ExternalProvider::class, $providers[ExternalProvider::ID]);
1293
+        self::assertCount(2, $providers);
1294
+    }
1295
+
1296
+    public function testGetProvidersIncludesExternalViaEvent() {
1297
+        // Arrange: No local providers, one external provider via event
1298
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([]);
1299
+        $this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([]);
1300
+        $this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([]);
1301
+        $this->registrationContext->expects($this->any())->method('getSpeechToTextProviders')->willReturn([]);
1302
+
1303
+
1304
+        $externalProvider = new ExternalProvider();
1305
+        $this->configureEventDispatcherMock(providersToAdd: [$externalProvider]);
1306
+        $this->manager = $this->createManagerInstance(); // Create manager with configured mocks
1307
+
1308
+        // Act
1309
+        $providers = $this->manager->getProviders(); // Returns ID-indexed array
1310
+
1311
+        // Assert
1312
+        self::assertArrayHasKey(ExternalProvider::ID, $providers);
1313
+        self::assertInstanceOf(ExternalProvider::class, $providers[ExternalProvider::ID]);
1314
+        self::assertCount(1, $providers);
1315
+        self::assertTrue($this->manager->hasProviders());
1316
+    }
1317
+
1318
+    public function testGetAvailableTaskTypesIncludesExternalViaEvent() {
1319
+        // Arrange: No local types/providers, one external type and provider via event
1320
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([]);
1321
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingTaskTypes')->willReturn([]);
1322
+        $this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([]);
1323
+        $this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([]);
1324
+        $this->registrationContext->expects($this->any())->method('getSpeechToTextProviders')->willReturn([]);
1325
+
1326
+        $externalProvider = new ExternalProvider(); // Provides ExternalTaskType
1327
+        $externalTaskType = new ExternalTaskType();
1328
+        $this->configureEventDispatcherMock(
1329
+            providersToAdd: [$externalProvider],
1330
+            taskTypesToAdd: [$externalTaskType]
1331
+        );
1332
+        $this->manager = $this->createManagerInstance();
1333
+
1334
+        // Act
1335
+        $availableTypes = $this->manager->getAvailableTaskTypes();
1336
+
1337
+        // Assert
1338
+        self::assertArrayHasKey(ExternalTaskType::ID, $availableTypes);
1339
+        self::assertContains(ExternalTaskType::ID, $this->manager->getAvailableTaskTypeIds());
1340
+        self::assertEquals(ExternalTaskType::ID, $externalProvider->getTaskTypeId(), 'Test Sanity: Provider must handle the Task Type');
1341
+        self::assertEquals('External Task Type via Event', $availableTypes[ExternalTaskType::ID]['name']);
1342
+        // Check if shapes match the external type/provider
1343
+        self::assertArrayHasKey('external_input', $availableTypes[ExternalTaskType::ID]['inputShape']);
1344
+        self::assertArrayHasKey('external_output', $availableTypes[ExternalTaskType::ID]['outputShape']);
1345
+        self::assertEmpty($availableTypes[ExternalTaskType::ID]['optionalInputShape']); // From ExternalProvider
1346
+    }
1347
+
1348
+    public function testLocalProviderWinsConflictWithEvent() {
1349
+        // Arrange: Local provider registered, conflicting external provider via event
1350
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
1351
+            new ServiceRegistration('test', SuccessfulSyncProvider::class)
1352
+        ]);
1353
+        $this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([]);
1354
+        $this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([]);
1355
+        $this->registrationContext->expects($this->any())->method('getSpeechToTextProviders')->willReturn([]);
1356
+
1357
+        $conflictingExternalProvider = new ConflictingExternalProvider(); // ID = 'test:sync:success'
1358
+        $this->configureEventDispatcherMock(providersToAdd: [$conflictingExternalProvider]);
1359
+        $this->manager = $this->createManagerInstance();
1360
+
1361
+        // Act
1362
+        $providers = $this->manager->getProviders();
1363
+
1364
+        // Assert: Only the local provider should be present for the conflicting ID
1365
+        self::assertArrayHasKey(SuccessfulSyncProvider::ID, $providers);
1366
+        self::assertInstanceOf(SuccessfulSyncProvider::class, $providers[SuccessfulSyncProvider::ID]);
1367
+        self::assertCount(1, $providers); // Ensure no extra provider was added
1368
+    }
1369
+
1370
+    public function testTriggerableProviderWithNoOtherRunningTasks() {
1371
+        // Arrange: Local provider registered, conflicting external provider via event
1372
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([]);
1373
+        $this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([]);
1374
+        $this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([]);
1375
+        $this->registrationContext->expects($this->any())->method('getSpeechToTextProviders')->willReturn([]);
1376
+
1377
+        $externalProvider = $this->createPartialMock(ExternalTriggerableProvider::class, ['trigger']);
1378
+        $externalProvider->expects($this->once())->method('trigger');
1379
+        $this->configureEventDispatcherMock(providersToAdd: [$externalProvider]);
1380
+        $this->manager = $this->createManagerInstance();
1381
+
1382
+        // Act
1383
+        $task = new Task($externalProvider->getTaskTypeId(), ['input' => ''], 'tests', null);
1384
+        $this->manager->scheduleTask($task);
1385
+    }
1386
+
1387
+    public function testTriggerableProviderWithOtherRunningTasks() {
1388
+        // Arrange: Local provider registered, conflicting external provider via event
1389
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([]);
1390
+        $this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([]);
1391
+        $this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([]);
1392
+        $this->registrationContext->expects($this->any())->method('getSpeechToTextProviders')->willReturn([]);
1393
+
1394
+        $externalProvider = $this->createPartialMock(ExternalTriggerableProvider::class, ['trigger']);
1395
+        $externalProvider->expects($this->once())->method('trigger');
1396
+        $this->configureEventDispatcherMock(providersToAdd: [$externalProvider]);
1397
+        $this->manager = $this->createManagerInstance();
1398
+
1399
+        $task = new Task($externalProvider->getTaskTypeId(), ['input' => ''], 'tests', null);
1400
+        $this->manager->scheduleTask($task);
1401
+        $this->manager->lockTask($task);
1402
+
1403
+        // Act
1404
+        $task = new Task($externalProvider->getTaskTypeId(), ['input' => ''], 'tests', null);
1405
+        $this->manager->scheduleTask($task);
1406
+    }
1407
+
1408
+    public function testMergeTaskTypesLocalAndEvent() {
1409
+        // Arrange: Local type registered, DIFFERENT external type via event
1410
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingProviders')->willReturn([
1411
+            new ServiceRegistration('test', AsyncProvider::class)
1412
+        ]);
1413
+        $this->registrationContext->expects($this->any())->method('getTaskProcessingTaskTypes')->willReturn([
1414
+            new ServiceRegistration('test', AudioToImage::class)
1415
+        ]);
1416
+        $this->registrationContext->expects($this->any())->method('getTextProcessingProviders')->willReturn([]);
1417
+        $this->registrationContext->expects($this->any())->method('getTextToImageProviders')->willReturn([]);
1418
+        $this->registrationContext->expects($this->any())->method('getSpeechToTextProviders')->willReturn([]);
1419
+
1420
+        $externalTaskType = new ExternalTaskType(); // ID = 'event:external:tasktype'
1421
+        $externalProvider = new ExternalProvider(); // Handles 'event:external:tasktype'
1422
+        $this->configureEventDispatcherMock(
1423
+            providersToAdd: [$externalProvider],
1424
+            taskTypesToAdd: [$externalTaskType]
1425
+        );
1426
+        $this->manager = $this->createManagerInstance();
1427
+
1428
+        // Act
1429
+        $availableTypes = $this->manager->getAvailableTaskTypes();
1430
+        $availableTypeIds = $this->manager->getAvailableTaskTypeIds();
1431
+
1432
+        // Assert: Both task types should be available
1433
+        self::assertContains(AudioToImage::ID, $availableTypeIds);
1434
+        self::assertArrayHasKey(AudioToImage::ID, $availableTypes);
1435
+        self::assertEquals(AudioToImage::class, $availableTypes[AudioToImage::ID]['name']);
1436
+
1437
+        self::assertContains(ExternalTaskType::ID, $availableTypeIds);
1438
+        self::assertArrayHasKey(ExternalTaskType::ID, $availableTypes);
1439
+        self::assertEquals('External Task Type via Event', $availableTypes[ExternalTaskType::ID]['name']);
1440
+
1441
+        self::assertCount(2, $availableTypes);
1442
+    }
1443
+
1444
+    private function createManagerInstance(): Manager {
1445
+        // Clear potentially cached config values if needed
1446
+        $this->appConfig->deleteKey('core', 'ai.taskprocessing_type_preferences');
1447
+
1448
+        // Re-create Text2ImageManager if its state matters or mocks change
1449
+        $text2imageManager = new \OC\TextToImage\Manager(
1450
+            $this->serverContainer,
1451
+            $this->coordinator,
1452
+            Server::get(LoggerInterface::class),
1453
+            $this->jobList,
1454
+            Server::get(\OC\TextToImage\Db\TaskMapper::class),
1455
+            $this->config, // Use the shared config mock
1456
+            Server::get(IAppDataFactory::class),
1457
+        );
1458
+
1459
+        return new Manager(
1460
+            $this->appConfig,
1461
+            $this->coordinator,
1462
+            $this->serverContainer,
1463
+            Server::get(LoggerInterface::class),
1464
+            $this->taskMapper,
1465
+            $this->jobList,
1466
+            $this->eventDispatcher, // Use the potentially reconfigured mock
1467
+            Server::get(IAppDataFactory::class),
1468
+            $this->rootFolder,
1469
+            $text2imageManager,
1470
+            $this->userMountCache,
1471
+            Server::get(IClientService::class),
1472
+            Server::get(IAppManager::class),
1473
+            Server::get(IUserManager::class),
1474
+            Server::get(IUserSession::class),
1475
+            Server::get(ICacheFactory::class),
1476
+            Server::get(IFactory::class),
1477
+        );
1478
+    }
1479
+
1480
+    private function configureEventDispatcherMock(
1481
+        array $providersToAdd = [],
1482
+        array $taskTypesToAdd = [],
1483
+        ?int $expectedCalls = null,
1484
+    ): void {
1485
+        $dispatchExpectation = $expectedCalls === null ? $this->any() : $this->exactly($expectedCalls);
1486
+
1487
+        $this->eventDispatcher->expects($dispatchExpectation)
1488
+            ->method('dispatchTyped')
1489
+            ->willReturnCallback(function (object $event) use ($providersToAdd, $taskTypesToAdd): void {
1490
+                if ($event instanceof GetTaskProcessingProvidersEvent) {
1491
+                    foreach ($providersToAdd as $providerInstance) {
1492
+                        $event->addProvider($providerInstance);
1493
+                    }
1494
+                    foreach ($taskTypesToAdd as $taskTypeInstance) {
1495
+                        $event->addTaskType($taskTypeInstance);
1496
+                    }
1497
+                }
1498
+            });
1499
+    }
1500 1500
 }
Please login to merge, or discard this patch.