Files
coolify/app/Models/NotificationThrottle.php
T
Andras Bacsai 9e7cb60b0d feat: add import restore options, OAuth confirmation, and fixes
- database import: add keep_owners (restore PostgreSQL owners and
  privileges) and sqlite_database (pick the target SQLite file) to the
  API, OpenAPI spec, and import form
- oauth: require password confirmation and support team selection on
  OAuth login
- remote process: send team broadcast events to the team of the server
  that ran the process instead of the causer's user id
- servers API: handle the legacy build server setting
- compose: support parent bind paths and volume driver options
- dns: improve record hints and provider management
- notifications: fix throttling and missing backup notifications
- ui: rework the confirmation modal
- add feature and unit tests for the new behavior
2026-09-30 13:35:22 +02:00

137 lines
3.9 KiB
PHP

<?php
namespace App\Models;
use App\Contracts\ThrottledNotification;
use Carbon\CarbonInterface;
use Closure;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Throwable;
/**
* Records when a notification was last sent for a resource, so repeated checks
* (across queue workers) do not send the same notification again.
*/
class NotificationThrottle extends Model
{
public $timestamps = false;
protected $fillable = [
'notifiable_type',
'notifiable_id',
'notification',
'sent_at',
];
protected function casts(): array
{
return [
'sent_at' => 'datetime',
];
}
public function notifiable(): MorphTo
{
return $this->morphTo();
}
/**
* Atomically reserve the right to send a notification.
*
* Returns true when the notification was never sent, or when it was last sent
* before $sentBefore. Without $sentBefore, it is sent only once until released.
*/
public static function claim(Model $notifiable, string $notification, ?CarbonInterface $sentBefore = null): bool
{
if ($sentBefore !== null) {
$renewed = static::query()
->whereMorphedTo('notifiable', $notifiable)
->where('notification', $notification)
->where('sent_at', '<', $sentBefore)
->update(['sent_at' => now()]);
if ($renewed === 1) {
return true;
}
}
return static::query()->insertOrIgnore([
'notifiable_type' => $notifiable->getMorphClass(),
'notifiable_id' => $notifiable->getKey(),
'notification' => $notification,
'sent_at' => now(),
]) === 1;
}
/**
* Claim the throttle, then run $send. When $send throws, the claim is released so the
* next attempt is not throttled, and the exception is rethrown.
*
* Returns false when the claim failed and $send did not run.
*/
public static function sendOnce(Model $notifiable, string $notification, ?CarbonInterface $sentBefore, Closure $send): bool
{
if (! static::claim($notifiable, $notification, $sentBefore)) {
return false;
}
try {
$send();
} catch (Throwable $exception) {
static::release($notifiable, $notification);
throw $exception;
}
return true;
}
/**
* Claim the throttle a notification declares. Notifications without a throttle subject are always allowed.
*/
public static function claimFor(ThrottledNotification $notification): bool
{
$subject = $notification->throttleSubject();
if ($subject === null) {
return true;
}
return static::claim($subject, $notification::class, now()->subMinutes($notification->throttleIntervalMinutes()));
}
/**
* Mark a notification as sent without sending it.
*/
public static function record(Model $notifiable, string $notification): void
{
static::query()->updateOrInsert(
[
'notifiable_type' => $notifiable->getMorphClass(),
'notifiable_id' => $notifiable->getKey(),
'notification' => $notification,
],
['sent_at' => now()],
);
}
/**
* Forget a sent notification. Returns true when a record was removed.
*/
public static function release(Model $notifiable, string $notification): bool
{
return static::query()
->whereMorphedTo('notifiable', $notifiable)
->where('notification', $notification)
->delete() > 0;
}
public static function wasSent(Model $notifiable, string $notification): bool
{
return static::query()
->whereMorphedTo('notifiable', $notifiable)
->where('notification', $notification)
->exists();
}
}