Skip to content

Commit

Permalink
Fair use of push notifications
Browse files Browse the repository at this point in the history
We want to keep offering our push notification service for free, but large
users overload our infrastructure. For this reason we have to rate-limit the
use of push notifications. If you need this feature, consider setting up your
own push server or using Nextcloud Enterprise.

Signed-off-by: Joas Schilling <coding@schilljs.com>
  • Loading branch information
nickvergessen committed Oct 22, 2021
1 parent fb18f29 commit 32b9bee
Show file tree
Hide file tree
Showing 8 changed files with 180 additions and 43 deletions.
4 changes: 3 additions & 1 deletion build/psalm-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4269,6 +4269,7 @@
<code>!($notification instanceof INotification)</code>
<code>!($notification instanceof INotification)</code>
</TypeDoesNotContainType>
<InvalidCatch occurrences="3" />
</file>
<file src="lib/private/Preview/BackgroundCleanupJob.php">
<InvalidReturnStatement occurrences="1">
Expand Down Expand Up @@ -4824,11 +4825,12 @@
<code>$this-&gt;createUserFromBackend($uid, $password, $backend)</code>
<code>$this-&gt;createUserFromBackend($uid, $password, $backend)</code>
</NullableReturnStatement>
<UndefinedInterfaceMethod occurrences="5">
<UndefinedInterfaceMethod occurrences="6">
<code>checkPassword</code>
<code>checkPassword</code>
<code>countUsers</code>
<code>createUser</code>
<code>delegateIsHardUserLimitReached</code>
<code>getUsersForUserValueCaseInsensitive</code>
</UndefinedInterfaceMethod>
</file>
Expand Down
78 changes: 60 additions & 18 deletions lib/private/Notification/Manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,10 @@
namespace OC\Notification;

use OC\AppFramework\Bootstrap\Coordinator;
use OCP\AppFramework\QueryException;
use OCP\ILogger;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\ICache;
use OCP\ICacheFactory;
use OCP\IUserManager;
use OCP\Notification\AlreadyProcessedException;
use OCP\Notification\IApp;
use OCP\Notification\IDeferrableApp;
Expand All @@ -37,11 +39,22 @@
use OCP\Notification\INotification;
use OCP\Notification\INotifier;
use OCP\RichObjectStrings\IValidator;
use OCP\Support\Subscription\IRegistry;
use Psr\Container\ContainerExceptionInterface;
use Psr\Log\LoggerInterface;

class Manager implements IManager {
/** @var IValidator */
protected $validator;
/** @var ILogger */
/** @var IUserManager */
private $userManager;
/** @var ICache */
protected $cache;
/** @var ITimeFactory */
protected $timeFactory;
/** @var IRegistry */
protected $subscription;
/** @var LoggerInterface */
protected $logger;
/** @var Coordinator */
private $coordinator;
Expand All @@ -64,9 +77,17 @@ class Manager implements IManager {
private $parsedRegistrationContext;

public function __construct(IValidator $validator,
ILogger $logger,
IUserManager $userManager,
ICacheFactory $cacheFactory,
ITimeFactory $timeFactory,
IRegistry $subscription,
LoggerInterface $logger,
Coordinator $coordinator) {
$this->validator = $validator;
$this->userManager = $userManager;
$this->cache = $cacheFactory->createDistributed('notifications');
$this->timeFactory = $timeFactory;
$this->subscription = $subscription;
$this->logger = $logger;
$this->coordinator = $coordinator;

Expand Down Expand Up @@ -97,9 +118,10 @@ public function registerApp(string $appClass): void {
*/
public function registerNotifier(\Closure $service, \Closure $info) {
$infoData = $info();
$this->logger->logException(new \InvalidArgumentException(
$exception = new \InvalidArgumentException(
'Notifier ' . $infoData['name'] . ' (id: ' . $infoData['id'] . ') is not considered because it is using the old way to register.'
));
);
$this->logger->error($exception->getMessage(), ['exception' => $exception]);
}

/**
Expand All @@ -121,10 +143,10 @@ protected function getApps(): array {

foreach ($this->appClasses as $appClass) {
try {
$app = \OC::$server->query($appClass);
} catch (QueryException $e) {
$this->logger->logException($e, [
'message' => 'Failed to load notification app class: ' . $appClass,
$app = \OC::$server->get($appClass);
} catch (ContainerExceptionInterface $e) {
$this->logger->error('Failed to load notification app class: ' . $appClass, [
'exception' => $e,
'app' => 'notifications',
]);
continue;
Expand Down Expand Up @@ -153,10 +175,10 @@ public function getNotifiers(): array {
$notifierServices = $this->coordinator->getRegistrationContext()->getNotifierServices();
foreach ($notifierServices as $notifierService) {
try {
$notifier = \OC::$server->query($notifierService->getService());
} catch (QueryException $e) {
$this->logger->logException($e, [
'message' => 'Failed to load notification notifier class: ' . $notifierService->getService(),
$notifier = \OC::$server->get($notifierService->getService());
} catch (ContainerExceptionInterface $e) {
$this->logger->error('Failed to load notification notifier class: ' . $notifierService->getService(), [
'exception' => $e,
'app' => 'notifications',
]);
continue;
Expand All @@ -181,10 +203,10 @@ public function getNotifiers(): array {

foreach ($this->notifierClasses as $notifierClass) {
try {
$notifier = \OC::$server->query($notifierClass);
} catch (QueryException $e) {
$this->logger->logException($e, [
'message' => 'Failed to load notification notifier class: ' . $notifierClass,
$notifier = \OC::$server->get($notifierClass);
} catch (ContainerExceptionInterface $e) {
$this->logger->error('Failed to load notification notifier class: ' . $notifierClass, [
'exception' => $e,
'app' => 'notifications',
]);
continue;
Expand Down Expand Up @@ -277,6 +299,26 @@ public function flush(): void {
$this->deferPushing = false;
}

/**
* {@inheritDoc}
*/
public function isFairUseOfFreePushService(): bool {
$pushAllowed = $this->cache->get('push_fair_use');
if ($pushAllowed === null) {
/**
* We want to keep offering our push notification service for free, but large
* users overload our infrastructure. For this reason we have to rate-limit the
* use of push notifications. If you need this feature, consider setting up your
* own push server or using Nextcloud Enterprise.
*/
// TODO Remove time check after 1st March 2022
$isFairUse = $this->timeFactory->getTime() < 1646089200 || $this->subscription->delegateHasValidSubscription() || $this->userManager->countSeenUsers() < 5000;
$pushAllowed = $isFairUse ? 'yes' : 'no';
$this->cache->set('push_fair_use', $pushAllowed, 3600);
}
return $pushAllowed === 'yes';
}

/**
* @param INotification $notification
* @throws \InvalidArgumentException When the notification is not valid
Expand Down
23 changes: 10 additions & 13 deletions lib/private/Support/Subscription/Registry.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,21 +59,17 @@ class Registry implements IRegistry {
private $groupManager;
/** @var LoggerInterface */
private $logger;
/** @var IManager */
private $notificationManager;

public function __construct(IConfig $config,
IServerContainer $container,
IUserManager $userManager,
IGroupManager $groupManager,
LoggerInterface $logger,
IManager $notificationManager) {
LoggerInterface $logger) {
$this->config = $config;
$this->container = $container;
$this->userManager = $userManager;
$this->groupManager = $groupManager;
$this->logger = $logger;
$this->notificationManager = $notificationManager;
}

private function getSubscription(): ?ISubscription {
Expand Down Expand Up @@ -158,15 +154,16 @@ public function delegateHasExtendedSupport(): bool {
/**
* Indicates if a hard user limit is reached and no new users should be created
*
* @param IManager|null $notificationManager
* @since 21.0.0
*/
public function delegateIsHardUserLimitReached(): bool {
public function delegateIsHardUserLimitReached(?IManager $notificationManager = null): bool {
$subscription = $this->getSubscription();
if ($subscription instanceof ISubscription &&
$subscription->hasValidSubscription()) {
$userLimitReached = $subscription->isHardUserLimitReached();
if ($userLimitReached) {
$this->notifyAboutReachedUserLimit();
if ($userLimitReached && $notificationManager instanceof IManager) {
$this->notifyAboutReachedUserLimit($notificationManager);
}
return $userLimitReached;
}
Expand All @@ -181,8 +178,8 @@ public function delegateIsHardUserLimitReached(): bool {
$hardUserLimit = $this->config->getSystemValue('one-click-instance.user-limit', 50);

$userLimitReached = $userCount >= $hardUserLimit;
if ($userLimitReached) {
$this->notifyAboutReachedUserLimit();
if ($userLimitReached && $notificationManager instanceof IManager) {
$this->notifyAboutReachedUserLimit($notificationManager);
}
return $userLimitReached;
}
Expand Down Expand Up @@ -216,17 +213,17 @@ private function getUserCount(): int {
return $userCount;
}

private function notifyAboutReachedUserLimit() {
private function notifyAboutReachedUserLimit(IManager $notificationManager) {
$admins = $this->groupManager->get('admin')->getUsers();
foreach ($admins as $admin) {
$notification = $this->notificationManager->createNotification();
$notification = $notificationManager->createNotification();

$notification->setApp('core')
->setUser($admin->getUID())
->setDateTime(new \DateTime())
->setObject('user_limit_reached', '1')
->setSubject('user_limit_reached');
$this->notificationManager->notify($notification);
$notificationManager->notify($notification);
}

$this->logger->warning('The user limit was reached and the new user was not created', ['app' => 'lib']);
Expand Down
7 changes: 6 additions & 1 deletion lib/private/User/Manager.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
use OCP\IUser;
use OCP\IUserBackend;
use OCP\IUserManager;
use OCP\Notification\IManager;
use OCP\Support\Subscription\IRegistry;
use OCP\User\Backend\IGetRealUIDBackend;
use OCP\User\Backend\ISearchKnownUsersBackend;
Expand Down Expand Up @@ -373,7 +374,11 @@ public function searchKnownUsersByDisplayName(string $searcher, string $pattern,
*/
public function createUser($uid, $password) {
// DI injection is not used here as IRegistry needs the user manager itself for user count and thus it would create a cyclic dependency
if (\OC::$server->get(IRegistry::class)->delegateIsHardUserLimitReached()) {
/** @var IRegistry $registry */
$registry = \OC::$server->get(IRegistry::class);
/** @var IManager $registry */
$notificationManager = \OC::$server->get(IManager::class);
if ($registry->delegateIsHardUserLimitReached($notificationManager)) {
$l = \OC::$server->getL10N('lib');
throw new HintException($l->t('The user limit has been reached and the user was not created.'));
}
Expand Down
12 changes: 12 additions & 0 deletions lib/public/Notification/IManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,4 +107,16 @@ public function defer(): bool;
* @since 20.0.0
*/
public function flush(): void;

/**
* Whether the server can use the hosted push notification service
*
* We want to keep offering our push notification service for free, but large
* users overload our infrastructure. For this reason we have to rate-limit the
* use of push notifications. If you need this feature, consider setting up your
* own push server or using Nextcloud Enterprise.
*
* @since 23.0.0
*/
public function isFairUseOfFreePushService(): bool;
}
4 changes: 3 additions & 1 deletion lib/public/Support/Subscription/IRegistry.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
*/
namespace OCP\Support\Subscription;

use OCP\Notification\IManager;
use OCP\Support\Subscription\Exception\AlreadyRegisteredException;

/**
Expand Down Expand Up @@ -81,7 +82,8 @@ public function delegateHasExtendedSupport(): bool;
/**
* Indicates if a hard user limit is reached and no new users should be created
*
* @param IManager|null $notificationManager
* @since 21.0.0
*/
public function delegateIsHardUserLimitReached(): bool;
public function delegateIsHardUserLimitReached(?IManager $notificationManager = null): bool;
}
Loading

0 comments on commit 32b9bee

Please sign in to comment.