Skip to content
Merged
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: 2 additions & 0 deletions lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
namespace OCA\OpenAi\AppInfo;

use OCA\OpenAi\Capabilities;
use OCA\OpenAi\Notification\Notifier;
use OCA\OpenAi\OldProcessing\Translation\TranslationProvider as OldTranslationProvider;
use OCA\OpenAi\TaskProcessing\AudioToAudioChatProvider;
use OCA\OpenAi\TaskProcessing\AudioToTextProvider;
Expand Down Expand Up @@ -152,6 +153,7 @@ public function register(IRegistrationContext $context): void {
}

$context->registerCapability(Capabilities::class);
$context->registerNotifierService(Notifier::class);
}

public function boot(IBootContext $context): void {
Expand Down
86 changes: 86 additions & 0 deletions lib/Notification/Notifier.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?php

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

namespace OCA\OpenAi\Notification;

use InvalidArgumentException;
use OCA\OpenAi\AppInfo\Application;
use OCP\IURLGenerator;
use OCP\L10N\IFactory;
use OCP\Notification\IAction;
use OCP\Notification\INotification;

use OCP\Notification\INotifier;

class Notifier implements INotifier {

public function __construct(
private IFactory $factory,
private IURLGenerator $url,
) {
}

public function getID(): string {
return Application::APP_ID;
}

public function getName(): string {
return $this->factory->get(Application::APP_ID)->t('OpenAI Integration');
}

public function prepare(INotification $notification, string $languageCode): INotification {
if ($notification->getApp() !== Application::APP_ID) {
// Not my app => throw
throw new InvalidArgumentException();
}
if ($notification->getSubject() !== 'quota_exceeded') {
// Not a valid subject => throw
throw new InvalidArgumentException();
}

$l = $this->factory->get(Application::APP_ID, $languageCode);

$params = $notification->getSubjectParameters();

$subject = $l->t('Quota exceeded');
$content = '';
switch ($params['type']) {
case Application::QUOTA_TYPE_TEXT:
$content = $l->t('Text generation quota exceeded');
break;
case Application::QUOTA_TYPE_IMAGE:
$content = $l->t('Image generation quota exceeded');
break;
case Application::QUOTA_TYPE_TRANSCRIPTION:
$content = $l->t('Audio transcription quota exceeded');
break;
case Application::QUOTA_TYPE_SPEECH:
$content = $l->t('Speech generation quota exceeded');
break;
}

$link = $this->url->getWebroot() . '/settings/user/ai';
$iconUrl = $this->url->getAbsoluteURL($this->url->imagePath(Application::APP_ID, 'app-dark.svg'));

$notification
->setParsedSubject($subject)
->setParsedMessage($content)
->setLink($link)
->setIcon($iconUrl);

$actionLabel = $params['actionLabel'] ?? $l->t('View quota');
$action = $notification->createAction();
$action->setLabel($actionLabel)
->setParsedLabel($actionLabel)
->setLink($notification->getLink(), IAction::TYPE_WEB)
->setPrimary(true);

$notification->addParsedAction($action);

return $notification;
}
}
20 changes: 18 additions & 2 deletions lib/Service/OpenAiAPIService.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

namespace OCA\OpenAi\Service;

use DateTime;
use Exception;
use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\ServerException;
Expand All @@ -25,6 +26,7 @@
use OCP\ICacheFactory;
use OCP\IL10N;
use OCP\Lock\LockedException;
use OCP\Notification\IManager as INotificationManager;
use OCP\TaskProcessing\ShapeEnumValue;
use Psr\Log\LoggerInterface;
use RuntimeException;
Expand All @@ -46,6 +48,7 @@ public function __construct(
private ICacheFactory $cacheFactory,
private QuotaUsageMapper $quotaUsageMapper,
private OpenAiSettingsService $openAiSettingsService,
private INotificationManager $notificationManager,
IClientService $clientService,
) {
$this->client = $clientService->newClient();
Expand Down Expand Up @@ -252,8 +255,21 @@ public function isQuotaExceeded(?string $userId, int $type): bool {
$this->logger->warning('Could not retrieve quota usage for user: ' . $userId . ' and quota type: ' . $type . '. Error: ' . $e->getMessage());
throw new Exception('Could not retrieve quota usage.', Http::STATUS_INTERNAL_SERVER_ERROR);
}

return $quotaUsage >= $quota;
if ($quotaUsage < $quota) {
return false;
}
$cache = $this->cacheFactory->createLocal(Application::APP_ID);
if ($cache->get('quota_exceeded_' . $userId . '_' . $type) === null) {
$notification = $this->notificationManager->createNotification();
$notification->setApp(Application::APP_ID)
->setUser($userId)
->setDateTime(new DateTime())
->setObject('quota_exceeded', (string)$type)
->setSubject('quota_exceeded', ['type' => $type]);
$this->notificationManager->notify($notification);
$cache->set('quota_exceeded_' . $userId . '_' . $type, true, 3600);
}
return true;
}

/**
Expand Down
39 changes: 20 additions & 19 deletions tests/unit/Providers/OpenAiProviderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,32 +56,33 @@ public static function setUpBeforeClass(): void {
parent::setUpBeforeClass();
$backend = new Dummy();
$backend->createUser(self::TEST_USER1, self::TEST_USER1);
\OC::$server->get(\OCP\IUserManager::class)->registerBackend($backend);
\OCP\Server::get(\OCP\IUserManager::class)->registerBackend($backend);
}

protected function setUp(): void {
parent::setUp();

$this->loginAsUser(self::TEST_USER1);

$this->openAiSettingsService = \OC::$server->get(OpenAiSettingsService::class);
$this->openAiSettingsService = \OCP\Server::get(OpenAiSettingsService::class);

$this->chunkService = \OC::$server->get(ChunkService::class);
$this->chunkService = \OCP\Server::get(ChunkService::class);

$this->quotaUsageMapper = \OC::$server->get(QuotaUsageMapper::class);
$this->quotaUsageMapper = \OCP\Server::get(QuotaUsageMapper::class);

// We'll hijack the client service and subsequently iClient to return a mock response from the OpenAI API
$clientService = $this->createMock(IClientService::class);
$this->iClient = $this->createMock(IClient::class);
$clientService->method('newClient')->willReturn($this->iClient);

$this->openAiApiService = new OpenAiAPIService(
\OC::$server->get(\Psr\Log\LoggerInterface::class),
\OCP\Server::get(\Psr\Log\LoggerInterface::class),
$this->createMock(\OCP\IL10N::class),
\OC::$server->get(IAppConfig::class),
\OC::$server->get(ICacheFactory::class),
\OC::$server->get(QuotaUsageMapper::class),
\OCP\Server::get(IAppConfig::class),
\OCP\Server::get(ICacheFactory::class),
\OCP\Server::get(QuotaUsageMapper::class),
$this->openAiSettingsService,
$this->createMock(\OCP\Notification\IManager::class),
$clientService,
);

Expand All @@ -90,7 +91,7 @@ protected function setUp(): void {

public static function tearDownAfterClass(): void {
// Delete quota usage for test user
$quotaUsageMapper = \OC::$server->get(QuotaUsageMapper::class);
$quotaUsageMapper = \OCP\Server::get(QuotaUsageMapper::class);
try {
$quotaUsageMapper->deleteUserQuotaUsages(self::TEST_USER1);
} catch (\OCP\Db\Exception|\RuntimeException|\Exception|\Throwable $e) {
Expand All @@ -99,15 +100,15 @@ public static function tearDownAfterClass(): void {

$backend = new \Test\Util\User\Dummy();
$backend->deleteUser(self::TEST_USER1);
\OC::$server->get(\OCP\IUserManager::class)->removeBackend($backend);
\OCP\Server::get(\OCP\IUserManager::class)->removeBackend($backend);

parent::tearDownAfterClass();
}

public function testFreePromptProvider(): void {
$freePromptProvider = new TextToTextProvider(
$this->openAiApiService,
\OC::$server->get(IAppConfig::class),
\OCP\Server::get(IAppConfig::class),
$this->openAiSettingsService,
$this->createMock(\OCP\IL10N::class),
self::TEST_USER1,
Expand Down Expand Up @@ -170,7 +171,7 @@ public function testFreePromptProvider(): void {
public function testEmojiProvider(): void {
$emojiProvider = new EmojiProvider(
$this->openAiApiService,
\OC::$server->get(IAppConfig::class),
\OCP\Server::get(IAppConfig::class),
$this->openAiSettingsService,
$this->createMock(\OCP\IL10N::class),
self::TEST_USER1,
Expand Down Expand Up @@ -235,7 +236,7 @@ public function testEmojiProvider(): void {
public function testHeadlineProvider(): void {
$headlineProvider = new HeadlineProvider(
$this->openAiApiService,
\OC::$server->get(IAppConfig::class),
\OCP\Server::get(IAppConfig::class),
$this->openAiSettingsService,
$this->createMock(\OCP\IL10N::class),
self::TEST_USER1,
Expand Down Expand Up @@ -299,7 +300,7 @@ public function testHeadlineProvider(): void {
public function testChangeToneProvider(): void {
$changeToneProvider = new ChangeToneProvider(
$this->openAiApiService,
\OC::$server->get(IAppConfig::class),
\OCP\Server::get(IAppConfig::class),
$this->openAiSettingsService,
$this->createMock(\OCP\IL10N::class),
$this->chunkService,
Expand Down Expand Up @@ -366,7 +367,7 @@ public function testChangeToneProvider(): void {
public function testSummaryProvider(): void {
$summaryProvider = new SummaryProvider(
$this->openAiApiService,
\OC::$server->get(IAppConfig::class),
\OCP\Server::get(IAppConfig::class),
$this->openAiSettingsService,
$this->createMock(\OCP\IL10N::class),
$this->chunkService,
Expand Down Expand Up @@ -435,7 +436,7 @@ public function testSummaryProvider(): void {
public function testProofreadProvider(): void {
$proofreadProvider = new ProofreadProvider(
$this->openAiApiService,
\OC::$server->get(IAppConfig::class),
\OCP\Server::get(IAppConfig::class),
$this->openAiSettingsService,
$this->createMock(\OCP\IL10N::class),
$this->chunkService,
Expand Down Expand Up @@ -503,10 +504,10 @@ public function testProofreadProvider(): void {
public function testTranslationProvider(): void {
$translationProvider = new TranslateProvider(
$this->openAiApiService,
\OC::$server->get(IAppConfig::class),
\OCP\Server::get(IAppConfig::class),
$this->openAiSettingsService,
$this->createMock(\OCP\IL10N::class),
\OC::$server->get(\OCP\L10N\IFactory::class),
\OCP\Server::get(\OCP\L10N\IFactory::class),
$this->createMock(\OCP\ICacheFactory::class),
$this->createMock(\Psr\Log\LoggerInterface::class),
$this->chunkService,
Expand Down Expand Up @@ -576,7 +577,7 @@ public function testTextToSpeechProvider(): void {
$this->openAiApiService,
$this->createMock(\OCP\IL10N::class),
$this->createMock(\Psr\Log\LoggerInterface::class),
\OC::$server->get(IAppConfig::class),
\OCP\Server::get(IAppConfig::class),
self::TEST_USER1,
);

Expand Down
Loading
Loading