Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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)
]]> </description>
<version>3.5.0-dev.1</version>
<version>3.5.0-dev.2</version>
<licence>agpl</licence>
<author>Julien Veyssier</author>
<namespace>Assistant</namespace>
Expand Down
17 changes: 13 additions & 4 deletions lib/Controller/AssignmentsApiController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<Http::STATUS_OK, array{assignment: AssistantAssignment}, array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_INTERNAL_SERVER_ERROR|Http::STATUS_BAD_REQUEST, '', array{}>
*
* 200: User assignments returned
Expand All @@ -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) {
Expand Down Expand Up @@ -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<Http::STATUS_OK, array{assignment: AssistantAssignment}, array{}>|DataResponse<Http::STATUS_FORBIDDEN|Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND|Http::STATUS_INTERNAL_SERVER_ERROR, '', array{}>
*
* 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);
Expand All @@ -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 */
Expand Down
22 changes: 21 additions & 1 deletion lib/Db/Assignment.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand All @@ -50,6 +51,9 @@ class Assignment extends Entity implements \JsonSerializable {
/** @var int */
protected $lastRunAt;

/** @var string */
protected $timezone;

public static $columns = [
'id',
'user_id',
Expand All @@ -59,6 +63,7 @@ class Assignment extends Entity implements \JsonSerializable {
'created_at',
'updated_at',
'last_run_at',
'timezone'
];
public static $fields = [
'id',
Expand All @@ -69,6 +74,7 @@ class Assignment extends Entity implements \JsonSerializable {
'createdAt',
'updatedAt',
'lastRunAt',
'timezone'
];

public function __construct() {
Expand All @@ -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]
Expand All @@ -92,6 +99,7 @@ public function jsonSerialize() {
'created_at' => $this->getCreatedAt(),
'updated_at' => $this->getUpdatedAt(),
'last_run_at' => $this->getLastRunAt(),
'timezone' => $this->getTimezone(),
];
}

Expand All @@ -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
*/
Expand All @@ -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 */
Expand Down
45 changes: 45 additions & 0 deletions lib/Migration/Version030500Date20260528083738.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

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

namespace OCA\Assistant\Migration;

use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\DB\Types;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;

class Version030500Date20260528083738 extends SimpleMigrationStep {

/**
* @param IOutput $output
* @param Closure(): ISchemaWrapper $schemaClosure
* @param array $options
* @return null|ISchemaWrapper
*/
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
$schemaChanged = false;

if ($schema->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;
}
}
3 changes: 2 additions & 1 deletion lib/ResponseDefinitions.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
7 changes: 6 additions & 1 deletion lib/Service/AssignmentsService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand All @@ -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) {
Expand Down
20 changes: 17 additions & 3 deletions openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"created_at",
"updated_at",
"starts_at",
"last_run_at"
"last_run_at",
"timezone"
],
"properties": {
"id": {
Expand Down Expand Up @@ -61,6 +62,9 @@
"last_run_at": {
"type": "integer",
"format": "int64"
},
"timezone": {
"type": "string"
}
}
},
Expand Down Expand Up @@ -5337,7 +5341,8 @@
"title",
"prompt",
"startsAt",
"recurrence"
"recurrence",
"timezone"
],
"properties": {
"title": {
Expand All @@ -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)"
}
}
}
Expand Down Expand Up @@ -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"
}
}
}
Expand Down Expand Up @@ -6023,7 +6037,7 @@
}
},
"400": {
"description": "Malformed recurrence rule",
"description": "Malformed input",
"content": {
"application/json": {
"schema": {
Expand Down
38 changes: 38 additions & 0 deletions tests/unit/Db/AssignmentTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

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

namespace OCA\Assistant\Tests;

use OCA\Assistant\Db\Assignment;

class AssignmentTest extends \PHPUnit\Framework\TestCase {

/**
* @dataProvider isDueDataProvider
*/
public function testIsDue(string $rrule, string $startsAt, string $lastRunAt, string $timezone, string $now, bool $expected) {
$assignment = new Assignment();
$assignment->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],
];
}
}
Loading