Skip to content

Commit 15beacb

Browse files
committed
feat: Implement quota rules
Signed-off-by: Lukas Schaefer <[email protected]>
1 parent 5d46c3d commit 15beacb

19 files changed

+1266
-28
lines changed

.eslintrc.cjs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,19 @@
55

66
module.exports = {
77
globals: {
8-
appVersion: true
8+
appVersion: true,
99
},
1010
parserOptions: {
11-
requireConfigFile: false
11+
requireConfigFile: false,
1212
},
1313
extends: [
14-
'@nextcloud'
14+
'@nextcloud',
1515
],
1616
rules: {
1717
'jsdoc/require-jsdoc': 'off',
1818
'jsdoc/tag-lines': 'off',
1919
'vue/first-attribute-linebreak': 'off',
20-
'import/extensions': 'off'
21-
}
20+
'import/extensions': 'off',
21+
'vue/no-v-model-argument': 'off',
22+
},
2223
}

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: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ class Application extends App implements IBootstrap {
6565
public const QUOTA_TYPE_IMAGE = 1;
6666
public const QUOTA_TYPE_TRANSCRIPTION = 2;
6767
public const QUOTA_TYPE_SPEECH = 3;
68+
public const QUOTA_FOR_USER = 0;
69+
public const QUOTA_FOR_GROUP = 1;
6870

6971
public const DEFAULT_QUOTAS = [
7072
self::QUOTA_TYPE_TEXT => 0, // 0 = unlimited
@@ -75,6 +77,7 @@ class Application extends App implements IBootstrap {
7577
];
7678

7779
public const MODELS_CACHE_KEY = 'models';
80+
public const QUOTA_RULES_CACHE_PREFIX = 'quota_rules';
7881
public const MODELS_CACHE_TTL = 60 * 30;
7982

8083
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: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
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 ReturnTypeWillChange;
15+
16+
/**
17+
* @method int getType()
18+
* @method void setType(int $type)
19+
* @method int getAmount()
20+
* @method void setAmount(int $amount)
21+
* @method int getPriority()
22+
* @method void setPriority(int $priority)
23+
* @method bool getPool()
24+
* @method void setPool(bool $pool)
25+
*/
26+
class QuotaRule extends Entity implements JsonSerializable {
27+
/** @var int */
28+
protected $type;
29+
/** @var int */
30+
protected $amount;
31+
/** @var int */
32+
protected $priority;
33+
/** @var bool */
34+
protected $pool;
35+
36+
public function __construct() {
37+
$this->addType('type', 'integer');
38+
$this->addType('amount', 'integer');
39+
$this->addType('priority', 'integer');
40+
$this->addType('pool', 'boolean');
41+
}
42+
43+
#[ReturnTypeWillChange]
44+
public function jsonSerialize() {
45+
return [
46+
'id' => $this->id,
47+
'type' => $this->type,
48+
'amount' => $this->amount,
49+
'priority' => $this->priority,
50+
'pool' => $this->pool
51+
];
52+
}
53+
}

lib/Db/QuotaRuleMapper.php

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

lib/Db/QuotaUsage.php

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

1010
namespace OCA\OpenAi\Db;
1111

12+
use JsonSerializable;
1213
use OCP\AppFramework\Db\Entity;
14+
use ReturnTypeWillChange;
1315

1416
/**
1517
* @method string getUserId()
@@ -20,8 +22,10 @@
2022
* @method void setUnits(int $units)
2123
* @method int getTimestamp()
2224
* @method void setTimestamp(int $timestamp)
25+
* @method int getPool()
26+
* @method void setPool(int $pool)
2327
*/
24-
class QuotaUsage extends Entity implements \JsonSerializable {
28+
class QuotaUsage extends Entity implements JsonSerializable {
2529
/** @var string */
2630
protected $userId;
2731
/** @var int */
@@ -30,22 +34,26 @@ class QuotaUsage extends Entity implements \JsonSerializable {
3034
protected $units;
3135
/** @var int */
3236
protected $timestamp;
37+
/** @var int */
38+
protected $pool;
3339

3440
public function __construct() {
3541
$this->addType('user_id', 'string');
3642
$this->addType('type', 'integer');
3743
$this->addType('units', 'integer');
3844
$this->addType('timestamp', 'integer');
45+
$this->addType('pool', 'integer');
3946
}
4047

41-
#[\ReturnTypeWillChange]
48+
#[ReturnTypeWillChange]
4249
public function jsonSerialize() {
4350
return [
4451
'id' => $this->id,
4552
'user_id' => $this->userId,
4653
'type' => $this->type,
4754
'units' => $this->units,
4855
'timestamp' => $this->timestamp,
56+
'pool' => $this->pool
4957
];
5058
}
5159
}

0 commit comments

Comments
 (0)