Skip to content

Commit 7a62579

Browse files
authored
Merge pull request #255 from nextcloud/feat/quota-rules
feat: Implement quota rules
2 parents 6b4e40e + 9e30765 commit 7a62579

23 files changed

+1373
-41
lines changed

appinfo/info.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ Negative:
101101
102102
Learn more about the Nextcloud Ethical AI Rating [in our blog](https://nextcloud.com/blog/nextcloud-ethical-ai-rating/).
103103
]]> </description>
104-
<version>3.7.1</version>
104+
<version>3.8.0</version>
105105
<licence>agpl</licence>
106106
<author>Julien Veyssier</author>
107107
<namespace>OpenAi</namespace>

appinfo/routes.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,9 @@
1616
['name' => 'openAiAPI#getModels', 'url' => '/models', 'verb' => 'GET'],
1717
['name' => 'openAiAPI#getUserQuotaInfo', 'url' => '/quota-info', 'verb' => 'GET'],
1818
['name' => 'openAiAPI#getAdminQuotaInfo', 'url' => '/admin-quota-info', 'verb' => 'GET'],
19+
20+
['name' => 'quotaRule#addRule', 'url' => '/quota/rule', 'verb' => 'POST'],
21+
['name' => 'quotaRule#updateRule', 'url' => '/quota/rule', 'verb' => 'PUT'],
22+
['name' => 'quotaRule#deleteRule', 'url' => '/quota/rule', 'verb' => 'DELETE'],
1923
],
2024
];

lib/AppInfo/Application.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ class Application extends App implements IBootstrap {
7777
];
7878

7979
public const MODELS_CACHE_KEY = 'models';
80+
public const QUOTA_RULES_CACHE_PREFIX = 'quota_rules';
8081
public const MODELS_CACHE_TTL = 60 * 30;
8182

8283
private IAppConfig $appConfig;
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
<?php
2+
3+
/**
4+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
namespace OCA\OpenAi\Controller;
9+
10+
use Exception;
11+
use OCA\OpenAi\Service\QuotaRuleService;
12+
use OCP\AppFramework\Controller;
13+
use OCP\AppFramework\Http;
14+
use OCP\AppFramework\Http\DataResponse;
15+
use OCP\IRequest;
16+
17+
class QuotaRuleController extends Controller {
18+
public function __construct(
19+
string $appName,
20+
IRequest $request,
21+
private QuotaRuleService $quotaRuleService,
22+
) {
23+
parent::__construct($appName, $request);
24+
}
25+
26+
/**
27+
* POST /rule Creates a new empty rule returning the value of the rule
28+
* @return DataResponse
29+
*/
30+
public function addRule(): DataResponse {
31+
try {
32+
$result = $this->quotaRuleService->addRule();
33+
return new DataResponse($result);
34+
} catch (Exception $e) {
35+
return new DataResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
36+
}
37+
}
38+
39+
/**
40+
* PUT /rule
41+
* @param int $id
42+
* @param array $rule expects: type, amount, priority, pool, entities[]
43+
* @return DataResponse
44+
*/
45+
public function updateRule(int $id, array $rule): DataResponse {
46+
if (!isset($rule['type']) || !is_int($rule['type'])) {
47+
return new DataResponse(['error' => 'Missing or invalid type'], Http::STATUS_BAD_REQUEST);
48+
}
49+
if (!isset($rule['amount']) || !is_int($rule['amount'])) {
50+
return new DataResponse(['error' => 'Missing or invalid amount'], Http::STATUS_BAD_REQUEST);
51+
}
52+
if (!isset($rule['priority']) || !is_int($rule['priority'])) {
53+
return new DataResponse(['error' => 'Missing or invalid priority'], Http::STATUS_BAD_REQUEST);
54+
}
55+
if (!isset($rule['pool']) || !is_bool($rule['pool'])) {
56+
return new DataResponse(['error' => 'Missing or invalid pool value'], Http::STATUS_BAD_REQUEST);
57+
}
58+
if (!isset($rule['entities']) || !is_array($rule['entities'])) {
59+
return new DataResponse(['error' => 'Missing or invalid entities'], Http::STATUS_BAD_REQUEST);
60+
}
61+
try {
62+
$result = $this->quotaRuleService->updateRule($id, $rule);
63+
return new DataResponse($result);
64+
} catch (Exception $e) {
65+
return new DataResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
66+
}
67+
}
68+
69+
/**
70+
* DELETE /rule
71+
* @param int $id
72+
* @return DataResponse
73+
*/
74+
public function deleteRule(int $id): DataResponse {
75+
try {
76+
$this->quotaRuleService->deleteRule($id);
77+
return new DataResponse('');
78+
} catch (Exception $e) {
79+
return new DataResponse(['error' => $e->getMessage()], Http::STATUS_BAD_REQUEST);
80+
}
81+
}
82+
}

lib/Db/QuotaRule.php

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\OpenAi\Db;
11+
12+
use JsonSerializable;
13+
use OCP\AppFramework\Db\Entity;
14+
use OCP\DB\Types;
15+
use ReturnTypeWillChange;
16+
17+
/**
18+
* @method int getType()
19+
* @method void setType(int $type)
20+
* @method int getAmount()
21+
* @method void setAmount(int $amount)
22+
* @method int getPriority()
23+
* @method void setPriority(int $priority)
24+
* @method int getPool()
25+
* @method void setPool(int $pool)
26+
*/
27+
class QuotaRule extends Entity implements JsonSerializable {
28+
/** @var int */
29+
protected $type;
30+
/** @var int */
31+
protected $amount;
32+
/** @var int */
33+
protected $priority;
34+
/** @var int */
35+
protected $pool;
36+
37+
public function __construct() {
38+
$this->addType('type', Types::INTEGER);
39+
$this->addType('amount', Types::INTEGER);
40+
$this->addType('priority', Types::INTEGER);
41+
$this->addType('pool', Types::INTEGER);
42+
}
43+
44+
#[ReturnTypeWillChange]
45+
public function jsonSerialize() {
46+
return [
47+
'id' => $this->getId(),
48+
'type' => $this->getType(),
49+
'amount' => $this->getAmount(),
50+
'priority' => $this->getPriority(),
51+
'pool' => $this->getPool()
52+
];
53+
}
54+
}

lib/Db/QuotaRuleMapper.php

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
<?php
2+
3+
/**
4+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: AGPL-3.0-or-later
6+
*/
7+
8+
namespace OCA\OpenAi\Db;
9+
10+
use OCP\AppFramework\Db\DoesNotExistException;
11+
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
12+
use OCP\AppFramework\Db\QBMapper;
13+
use OCP\DB\Exception;
14+
use OCP\DB\QueryBuilder\IQueryBuilder;
15+
use OCP\IDBConnection;
16+
17+
/**
18+
* @extends QBMapper<QuotaRule>
19+
*/
20+
class QuotaRuleMapper extends QBMapper {
21+
public function __construct(
22+
IDBConnection $db,
23+
) {
24+
parent::__construct($db, 'openai_quota_rule', QuotaRule::class);
25+
}
26+
27+
/**
28+
* @return array
29+
* @throws Exception
30+
*/
31+
public function getRules(): array {
32+
$qb = $this->db->getQueryBuilder();
33+
34+
$qb->select('*')
35+
->from($this->getTableName());
36+
37+
return $this->findEntities($qb);
38+
}
39+
40+
/**
41+
* @param int $quotaType
42+
* @param string $userId
43+
* @param array $groups
44+
* @return QuotaRule
45+
* @throws DoesNotExistException
46+
* @throws Exception
47+
* @throws MultipleObjectsReturnedException
48+
*/
49+
public function getRule(int $quotaType, string $userId, array $groups): QuotaRule {
50+
$qb = $this->db->getQueryBuilder();
51+
52+
$qb->select('r.*')
53+
->from($this->getTableName(), 'r')
54+
->leftJoin('r', 'openai_quota_user', 'u', 'r.id = u.rule_id')
55+
->where(
56+
$qb->expr()->eq('r.type', $qb->createNamedParameter($quotaType, IQueryBuilder::PARAM_INT))
57+
)->andWhere(
58+
$qb->expr()->orX(
59+
$qb->expr()->andX(
60+
$qb->expr()->eq('u.entity_type', $qb->createNamedParameter(EntityType::USER->value, IQueryBuilder::PARAM_INT)),
61+
$qb->expr()->eq('u.entity_id', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR))
62+
),
63+
$qb->expr()->andX(
64+
$qb->expr()->eq('u.entity_type', $qb->createNamedParameter(EntityType::GROUP->value, IQueryBuilder::PARAM_INT)),
65+
$qb->expr()->in('u.entity_id', $qb->createNamedParameter($groups, IQueryBuilder::PARAM_STR_ARRAY))
66+
),
67+
68+
)
69+
)->orderBy('r.priority', 'ASC')
70+
->setMaxResults(1);
71+
/** @var QuotaRule $entity */
72+
$entity = $this->findEntity($qb);
73+
return $entity;
74+
}
75+
/**
76+
* @param int $quotaType
77+
* @param int $amount
78+
* @param int $priority
79+
* @param int $pool
80+
* @return int
81+
* @throws Exception
82+
*/
83+
public function addRule(int $quotaType, int $amount, int $priority, int $pool): int {
84+
$qb = $this->db->getQueryBuilder();
85+
86+
$qb->insert($this->getTableName())
87+
->values(
88+
[
89+
'type' => $qb->createNamedParameter($quotaType, IQueryBuilder::PARAM_INT),
90+
'amount' => $qb->createNamedParameter($amount, IQueryBuilder::PARAM_INT),
91+
'priority' => $qb->createNamedParameter($priority, IQueryBuilder::PARAM_INT),
92+
'pool' => $qb->createNamedParameter($pool, IQueryBuilder::PARAM_INT)
93+
]
94+
);
95+
$qb->executeStatement();
96+
return $qb->getLastInsertId();
97+
}
98+
/**
99+
* @param int $id
100+
* @param int $quotaType
101+
* @param int $amount
102+
* @param int $priority
103+
* @param int $pool
104+
* @return void
105+
* @throws Exception
106+
*/
107+
public function updateRule(int $id, int $quotaType, int $amount, int $priority, int $pool): void {
108+
$qb = $this->db->getQueryBuilder();
109+
110+
$qb->update($this->getTableName())
111+
->set('type', $qb->createNamedParameter($quotaType, IQueryBuilder::PARAM_INT))
112+
->set('amount', $qb->createNamedParameter($amount, IQueryBuilder::PARAM_INT))
113+
->set('priority', $qb->createNamedParameter($priority, IQueryBuilder::PARAM_INT))
114+
->set('pool', $qb->createNamedParameter($pool, IQueryBuilder::PARAM_INT))
115+
->where(
116+
$qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT))
117+
);
118+
$qb->executeStatement();
119+
}
120+
/**
121+
* @param int $id
122+
* @throws Exception
123+
*/
124+
public function deleteRule(int $id): void {
125+
$qb = $this->db->getQueryBuilder();
126+
127+
$qb->delete($this->getTableName())
128+
->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
129+
$qb->executeStatement();
130+
}
131+
}

lib/Db/QuotaUsage.php

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99

1010
namespace OCA\OpenAi\Db;
1111

12+
use JsonSerializable;
1213
use OCP\AppFramework\Db\Entity;
14+
use OCP\DB\Types;
15+
use ReturnTypeWillChange;
1316

1417
/**
1518
* @method string getUserId()
@@ -20,8 +23,10 @@
2023
* @method void setUnits(int $units)
2124
* @method int getTimestamp()
2225
* @method void setTimestamp(int $timestamp)
26+
* @method int getPool()
27+
* @method void setPool(int $pool)
2328
*/
24-
class QuotaUsage extends Entity implements \JsonSerializable {
29+
class QuotaUsage extends Entity implements JsonSerializable {
2530
/** @var string */
2631
protected $userId;
2732
/** @var int */
@@ -30,22 +35,26 @@ class QuotaUsage extends Entity implements \JsonSerializable {
3035
protected $units;
3136
/** @var int */
3237
protected $timestamp;
38+
/** @var int */
39+
protected $pool;
3340

3441
public function __construct() {
35-
$this->addType('user_id', 'string');
36-
$this->addType('type', 'integer');
37-
$this->addType('units', 'integer');
38-
$this->addType('timestamp', 'integer');
42+
$this->addType('user_id', Types::STRING);
43+
$this->addType('type', Types::INTEGER);
44+
$this->addType('units', Types::INTEGER);
45+
$this->addType('timestamp', Types::INTEGER);
46+
$this->addType('pool', Types::INTEGER);
3947
}
4048

41-
#[\ReturnTypeWillChange]
49+
#[ReturnTypeWillChange]
4250
public function jsonSerialize() {
4351
return [
44-
'id' => $this->id,
45-
'user_id' => $this->userId,
46-
'type' => $this->type,
47-
'units' => $this->units,
48-
'timestamp' => $this->timestamp,
52+
'id' => $this->getId(),
53+
'user_id' => $this->getUserId(),
54+
'type' => $this->getType(),
55+
'units' => $this->getUnits(),
56+
'timestamp' => $this->getTimestamp(),
57+
'pool' => $this->getPool()
4958
];
5059
}
5160
}

0 commit comments

Comments
 (0)