diff --git a/appinfo/info.xml b/appinfo/info.xml
index 223938c8..9926c48a 100644
--- a/appinfo/info.xml
+++ b/appinfo/info.xml
@@ -62,7 +62,7 @@ Known providers:
More details on how to set this up in the [admin docs](https://docs.nextcloud.com/server/latest/admin_manual/ai/index.html)
]]>
- 3.5.0-dev.1
+ 3.5.0-dev.2
agpl
Julien Veyssier
Assistant
diff --git a/lib/Controller/AssignmentsApiController.php b/lib/Controller/AssignmentsApiController.php
index ee556d44..35ac6324 100644
--- a/lib/Controller/AssignmentsApiController.php
+++ b/lib/Controller/AssignmentsApiController.php
@@ -49,6 +49,7 @@ public function __construct(
* @param string $prompt The prompt to be sent to the assistant when the assignment is executed
* @param int $startsAt The timestamp when the assignment should start being executed
* @param string $recurrence The recurrence rule for the assignment, in RRULE format (e.g. "FREQ=DAILY;INTERVAL=1" for a daily assignment)
+ * @param string $timezone The timezone for this assignment (either the timezone name or a timezone offset, e.g. "Europe/Berlin" or "+0100" for UTC+1)
* @return DataResponse|DataResponse
*
* 200: User assignments returned
@@ -58,9 +59,9 @@ public function __construct(
#[NoAdminRequired]
#[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT, tags: ['assignments'])]
#[Http\Attribute\ApiRoute(verb: 'POST', url: '/assignments')]
- public function createUserAssignment(string $title, string $prompt, int $startsAt, string $recurrence): DataResponse {
+ public function createUserAssignment(string $title, string $prompt, int $startsAt, string $recurrence, string $timezone): DataResponse {
try {
- $assignment = $this->assignmentsService->createAssignment($this->userId, $title, $prompt, $startsAt, $recurrence);
+ $assignment = $this->assignmentsService->createAssignment($this->userId, $title, $prompt, $startsAt, $recurrence, $timezone);
$serializedAssignment = $assignment->jsonSerialize();
return new DataResponse(['assignment' => $serializedAssignment]);
} catch (InternalException $e) {
@@ -141,18 +142,19 @@ public function getUserAssignment(int $id): DataResponse {
* @param string|null $prompt The prompt to be sent to the assistant when the assignment is executed
* @param int|null $startsAt The timestamp when the assignment should start being executed
* @param string|null $recurrence The recurrence rule for the assignment, in RRULE format
+ * @param string|null $timezone The timezone for this assignment, omit to leave the current value in place. the value should be either the timezone name or a timezone offset, e.g. "Europe/Berlin" or "+0100" for UTC+1
*
* @return DataResponse|DataResponse
*
* 200: User tasks returned
* 403: User not logged in
- * 400: Malformed recurrence rule
+ * 400: Malformed input
* 404: Assignment not found
*/
#[NoAdminRequired]
#[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT, tags: ['assignments'])]
#[Http\Attribute\ApiRoute(verb: 'PATCH', url: '/assignments/{id}')]
- public function updateUserAssignment(int $id, ?string $prompt, ?string $recurrence, ?int $startsAt): DataResponse {
+ public function updateUserAssignment(int $id, ?string $prompt, ?string $recurrence, ?int $startsAt, ?string $timezone): DataResponse {
if ($this->userId !== null) {
try {
$assignment = $this->assignmentMapper->find($this->userId, $id);
@@ -169,6 +171,13 @@ public function updateUserAssignment(int $id, ?string $prompt, ?string $recurren
if ($startsAt !== null) {
$assignment->setStartsAt($startsAt);
}
+ if ($timezone !== null) {
+ try {
+ $assignment->setTimezone($timezone);
+ } catch (\InvalidArgumentException $e) {
+ return new DataResponse('', Http::STATUS_BAD_REQUEST);
+ }
+ }
$assignment->setUpdatedAt($this->timeFactory->now()->getTimestamp());
$this->assignmentMapper->update($assignment);
/** @var AssistantAssignment $serializedAssignment */
diff --git a/lib/Db/Assignment.php b/lib/Db/Assignment.php
index ee4e03bc..b4b79a6b 100644
--- a/lib/Db/Assignment.php
+++ b/lib/Db/Assignment.php
@@ -33,6 +33,7 @@
* @method \void setUpdatedAt(int $updatedAt)
* @method \void setLastRunAt(int $lastRunAt)
* @method \int getLastRunAt()
+ * @method \string getTimezone()
*/
class Assignment extends Entity implements \JsonSerializable {
/** @var string */
@@ -50,6 +51,9 @@ class Assignment extends Entity implements \JsonSerializable {
/** @var int */
protected $lastRunAt;
+ /** @var string */
+ protected $timezone;
+
public static $columns = [
'id',
'user_id',
@@ -59,6 +63,7 @@ class Assignment extends Entity implements \JsonSerializable {
'created_at',
'updated_at',
'last_run_at',
+ 'timezone'
];
public static $fields = [
'id',
@@ -69,6 +74,7 @@ class Assignment extends Entity implements \JsonSerializable {
'createdAt',
'updatedAt',
'lastRunAt',
+ 'timezone'
];
public function __construct() {
@@ -79,6 +85,7 @@ public function __construct() {
$this->addType('createdAt', Types::BIGINT);
$this->addType('updatedAt', Types::BIGINT);
$this->addType('lastRunAt', Types::BIGINT);
+ $this->addType('timezone', Types::STRING);
}
#[\ReturnTypeWillChange]
@@ -92,6 +99,7 @@ public function jsonSerialize() {
'created_at' => $this->getCreatedAt(),
'updated_at' => $this->getUpdatedAt(),
'last_run_at' => $this->getLastRunAt(),
+ 'timezone' => $this->getTimezone(),
];
}
@@ -107,6 +115,18 @@ public function setRecurrence(string $recurrence): void {
$this->setter('recurrence', [$recurrence]);
}
+ /**
+ * @throws \InvalidArgumentException
+ */
+ public function setTimezone(string $timezone): void {
+ try {
+ $tz = new \DateTimeZone($timezone);
+ } catch (\Throwable $e) {
+ throw new \InvalidArgumentException('Invalid timezone: ' . $timezone, previous: $e);
+ }
+ $this->setter('timezone', [$tz->getName()]);
+ }
+
/**
* Evaluates the recurrence rule and checks if a run is due
*/
@@ -115,7 +135,7 @@ public function isDueToRun(\DateTimeImmutable $now): bool {
$startsAt = new \DateTime('@' . $this->getStartsAt());
$lastRunAt = new \DateTime('@' . $this->getLastRunAt());
// Find recurrences after the last run or after the current time if this assignment has never run
- $rule = new Rule($this->getRecurrence(), $startsAt);
+ $rule = new Rule($this->getRecurrence(), $startsAt, timezone: $this->getTimezone());
$transformer = new \Recurr\Transformer\ArrayTransformer();
$constraint = new AfterConstraint($this->getLastRunAt() !== 0 ? $lastRunAt : $startsAt, false);
/** @var RecurrenceCollection $collection */
diff --git a/lib/Migration/Version030500Date20260528083738.php b/lib/Migration/Version030500Date20260528083738.php
new file mode 100644
index 00000000..28360426
--- /dev/null
+++ b/lib/Migration/Version030500Date20260528083738.php
@@ -0,0 +1,45 @@
+hasTable('assistant_assignments')) {
+ $table = $schema->getTable('assistant_assignments');
+ if (!$table->hasColumn('timezone')) {
+ $table->addColumn('timezone', Types::STRING, [
+ 'notnull' => true,
+ 'length' => 256,
+ 'default' => 'UTC',
+ ]);
+ $schemaChanged = true;
+ }
+ }
+
+ return $schemaChanged ? $schema : null;
+ }
+}
diff --git a/lib/ResponseDefinitions.php b/lib/ResponseDefinitions.php
index 90015c5b..1a964f18 100644
--- a/lib/ResponseDefinitions.php
+++ b/lib/ResponseDefinitions.php
@@ -94,7 +94,8 @@
* created_at: int,
* updated_at: int,
* starts_at: int,
- * last_run_at: int
+ * last_run_at: int,
+ * timezone: string,
* }
*/
class ResponseDefinitions {
diff --git a/lib/Service/AssignmentsService.php b/lib/Service/AssignmentsService.php
index 631b6f70..c00af915 100644
--- a/lib/Service/AssignmentsService.php
+++ b/lib/Service/AssignmentsService.php
@@ -39,7 +39,7 @@ public function __construct(
* @throws UnauthorizedException
* @throws BadRequestException
*/
- public function createAssignment(?string $userId, string $title, string $prompt, int $startsAt, string $recurrence): Assignment {
+ public function createAssignment(?string $userId, string $title, string $prompt, int $startsAt, string $recurrence, string $timezone): Assignment {
if ($userId === null) {
throw new UnauthorizedException();
}
@@ -56,6 +56,11 @@ public function createAssignment(?string $userId, string $title, string $prompt,
} catch (\InvalidArgumentException $e) {
throw new BadRequestException('Invalid recurrence rule', previous: $e);
}
+ try {
+ $assignment->setTimezone($timezone);
+ } catch (\InvalidArgumentException $e) {
+ throw new BadRequestException('Invalid recurrence rule', previous: $e);
+ }
try {
$this->assignmentMapper->insert($assignment);
} catch (Exception $e) {
diff --git a/openapi.json b/openapi.json
index 310f79b7..314c6213 100644
--- a/openapi.json
+++ b/openapi.json
@@ -30,7 +30,8 @@
"created_at",
"updated_at",
"starts_at",
- "last_run_at"
+ "last_run_at",
+ "timezone"
],
"properties": {
"id": {
@@ -61,6 +62,9 @@
"last_run_at": {
"type": "integer",
"format": "int64"
+ },
+ "timezone": {
+ "type": "string"
}
}
},
@@ -5337,7 +5341,8 @@
"title",
"prompt",
"startsAt",
- "recurrence"
+ "recurrence",
+ "timezone"
],
"properties": {
"title": {
@@ -5356,6 +5361,10 @@
"recurrence": {
"type": "string",
"description": "The recurrence rule for the assignment, in RRULE format (e.g. \"FREQ=DAILY;INTERVAL=1\" for a daily assignment)"
+ },
+ "timezone": {
+ "type": "string",
+ "description": "The timezone for this assignment (either the timezone name or a timezone offset, e.g. \"Europe/Berlin\" or \"+0100\" for UTC+1)"
}
}
}
@@ -5925,6 +5934,11 @@
"format": "int64",
"nullable": true,
"description": "The timestamp when the assignment should start being executed"
+ },
+ "timezone": {
+ "type": "string",
+ "nullable": true,
+ "description": "The timezone for this assignment, omit to leave the current value in place. the value should be either the timezone name or a timezone offset, e.g. \"Europe/Berlin\" or \"+0100\" for UTC+1"
}
}
}
@@ -6023,7 +6037,7 @@
}
},
"400": {
- "description": "Malformed recurrence rule",
+ "description": "Malformed input",
"content": {
"application/json": {
"schema": {
diff --git a/tests/unit/Db/AssignmentTest.php b/tests/unit/Db/AssignmentTest.php
new file mode 100644
index 00000000..972b705a
--- /dev/null
+++ b/tests/unit/Db/AssignmentTest.php
@@ -0,0 +1,38 @@
+setRecurrence($rrule);
+ $assignment->setStartsAt((new \DateTimeImmutable($startsAt))->getTimestamp());
+ $assignment->setLastRunAt((new \DateTimeImmutable($lastRunAt))->getTimestamp());
+ $assignment->setTimezone($timezone);
+ self::assertEquals($expected, $assignment->isDueToRun(new \DateTimeImmutable($now)));
+ }
+
+ public function isDueDataProvider(): array {
+ return [
+ ['FREQ=DAILY', '@0', '@0', 'UTC', '1970-01-02T00:00:00Z', true],
+ ['FREQ=DAILY;BYHOUR=8', '@0', '@0', 'UTC', '1970-01-01T08:00:00Z', true],
+ ['FREQ=DAILY;BYHOUR=8', '@0', '1970-01-01T08:00:00Z', 'UTC', '1970-01-01T08:00:01Z', false],
+ ['FREQ=DAILY;BYHOUR=8', '@0', '@0', '+0200', '1970-01-01T10:00:00Z', true],
+ ['FREQ=DAILY;BYHOUR=8', '@0', '1970-01-01T10:00:00Z', '+0200', '1970-01-01T10:00:00Z', false],
+ ['FREQ=DAILY;BYHOUR=8', '@0', '1970-01-01T10:00:00Z', '+0200', '1970-01-02T10:00:00Z', true],
+ ['FREQ=DAILY;BYHOUR=8', '@0', '1970-01-01T10:00:00Z', '+0200', '1970-01-02T10:00:00Z', true],
+ ['FREQ=DAILY;BYHOUR=8', '@0', '1970-01-01T10:00:00Z', '+0200', '2027-01-02T10:00:00Z', true],
+ ];
+ }
+}