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