mirror of
https://github.com/maelgangloff/domain-watchdog.git
synced 2025-12-29 16:15:04 +00:00
Merged master
This commit is contained in:
@@ -2,22 +2,19 @@
|
||||
|
||||
namespace App\Config;
|
||||
|
||||
use App\Service\Connector\OvhConnector;
|
||||
use App\Service\Connector\GandiConnector;
|
||||
use App\Service\Connector\NamecheapConnector;
|
||||
use App\Config\Provider\GandiProvider;
|
||||
use App\Config\Provider\OvhProvider;
|
||||
|
||||
enum ConnectorProvider: string
|
||||
{
|
||||
case OVH = 'ovh';
|
||||
case GANDI = 'gandi';
|
||||
case NAMECHEAP = 'namecheap';
|
||||
|
||||
public function getConnectorProvider(): string
|
||||
{
|
||||
return match ($this) {
|
||||
ConnectorProvider::OVH => OvhConnector::class,
|
||||
ConnectorProvider::GANDI => GandiConnector::class,
|
||||
ConnectorProvider::NAMECHEAP => NamecheapConnector::class
|
||||
ConnectorProvider::OVH => OvhProvider::class,
|
||||
ConnectorProvider::GANDI => GandiProvider::class
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,4 +5,5 @@ namespace App\Config;
|
||||
enum TriggerAction: string
|
||||
{
|
||||
case SendEmail = 'email';
|
||||
case SendChat = 'chat';
|
||||
}
|
||||
|
||||
47
src/Config/WebhookScheme.php
Normal file
47
src/Config/WebhookScheme.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Config;
|
||||
|
||||
use Symfony\Component\Notifier\Bridge\Discord\DiscordTransportFactory;
|
||||
use Symfony\Component\Notifier\Bridge\Engagespot\EngagespotTransportFactory;
|
||||
use Symfony\Component\Notifier\Bridge\GoogleChat\GoogleChatTransportFactory;
|
||||
use Symfony\Component\Notifier\Bridge\Mattermost\MattermostTransportFactory;
|
||||
use Symfony\Component\Notifier\Bridge\MicrosoftTeams\MicrosoftTeamsTransportFactory;
|
||||
use Symfony\Component\Notifier\Bridge\Ntfy\NtfyTransportFactory;
|
||||
use Symfony\Component\Notifier\Bridge\Pushover\PushoverTransportFactory;
|
||||
use Symfony\Component\Notifier\Bridge\RocketChat\RocketChatTransportFactory;
|
||||
use Symfony\Component\Notifier\Bridge\Slack\SlackTransportFactory;
|
||||
use Symfony\Component\Notifier\Bridge\Telegram\TelegramTransportFactory;
|
||||
use Symfony\Component\Notifier\Bridge\Zulip\ZulipTransportFactory;
|
||||
|
||||
enum WebhookScheme: string
|
||||
{
|
||||
case DISCORD = 'discord';
|
||||
case GOOGLE_CHAT = 'googlechat';
|
||||
case MATTERMOST = 'mattermost';
|
||||
case MICROSOFT_TEAMS = 'microsoftteams';
|
||||
case ROCKET_CHAT = 'rocketchat';
|
||||
case SLACK = 'slack';
|
||||
case TELEGRAM = 'telegram';
|
||||
case ZULIP = 'zulip';
|
||||
case PUSHOVER = 'pushover';
|
||||
case NTFY = 'ntfy';
|
||||
case ENGAGESPOT = 'engagespot';
|
||||
|
||||
public function getChatTransportFactory(): string
|
||||
{
|
||||
return match ($this) {
|
||||
WebhookScheme::DISCORD => DiscordTransportFactory::class,
|
||||
WebhookScheme::GOOGLE_CHAT => GoogleChatTransportFactory::class,
|
||||
WebhookScheme::MATTERMOST => MattermostTransportFactory::class,
|
||||
WebhookScheme::MICROSOFT_TEAMS => MicrosoftTeamsTransportFactory::class,
|
||||
WebhookScheme::ROCKET_CHAT => RocketChatTransportFactory::class,
|
||||
WebhookScheme::SLACK => SlackTransportFactory::class,
|
||||
WebhookScheme::TELEGRAM => TelegramTransportFactory::class,
|
||||
WebhookScheme::ZULIP => ZulipTransportFactory::class,
|
||||
WebhookScheme::PUSHOVER => PushoverTransportFactory::class,
|
||||
WebhookScheme::NTFY => NtfyTransportFactory::class,
|
||||
WebhookScheme::ENGAGESPOT => EngagespotTransportFactory::class
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -2,17 +2,17 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Config\Connector\ConnectorInterface;
|
||||
use App\Entity\Connector;
|
||||
use App\Entity\User;
|
||||
use App\Service\Connector\AbstractProvider;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
class ConnectorController extends AbstractController
|
||||
@@ -43,7 +43,6 @@ class ConnectorController extends AbstractController
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
* @throws TransportExceptionInterface
|
||||
*/
|
||||
#[Route(
|
||||
path: '/api/connectors',
|
||||
@@ -69,10 +68,10 @@ class ConnectorController extends AbstractController
|
||||
]);
|
||||
|
||||
if (null === $provider) {
|
||||
throw new \Exception('Provider not found');
|
||||
throw new BadRequestHttpException('Provider not found');
|
||||
}
|
||||
|
||||
/** @var ConnectorInterface $connectorProviderClass */
|
||||
/** @var AbstractProvider $connectorProviderClass */
|
||||
$connectorProviderClass = $provider->getConnectorProvider();
|
||||
|
||||
$authData = $connectorProviderClass::verifyAuthData($connector->getAuthData(), $client);
|
||||
|
||||
@@ -4,7 +4,7 @@ namespace App\Controller;
|
||||
|
||||
use App\Entity\Domain;
|
||||
use App\Entity\WatchList;
|
||||
use App\Message\ProcessDomainTrigger;
|
||||
use App\Message\SendDomainEventNotif;
|
||||
use App\Repository\DomainRepository;
|
||||
use App\Service\RDAPService;
|
||||
use Psr\Log\LoggerInterface;
|
||||
@@ -25,7 +25,7 @@ class DomainRefreshController extends AbstractController
|
||||
private readonly RDAPService $RDAPService,
|
||||
private readonly RateLimiterFactory $rdapRequestsLimiter,
|
||||
private readonly MessageBusInterface $bus,
|
||||
private readonly LoggerInterface $logger
|
||||
private readonly LoggerInterface $logger, private readonly KernelInterface $kernel
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -35,8 +35,9 @@ class DomainRefreshController extends AbstractController
|
||||
* @throws ExceptionInterface
|
||||
* @throws \Exception
|
||||
* @throws HttpExceptionInterface
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function __invoke(string $ldhName, KernelInterface $kernel): ?Domain
|
||||
public function __invoke(string $ldhName, KernelInterface $kernel): Domain
|
||||
{
|
||||
$idnDomain = strtolower(idn_to_ascii($ldhName));
|
||||
$userId = $this->getUser()->getUserIdentifier();
|
||||
@@ -53,7 +54,8 @@ class DomainRefreshController extends AbstractController
|
||||
if (null !== $domain
|
||||
&& !$domain->getDeleted()
|
||||
&& ($domain->getUpdatedAt()->diff(new \DateTimeImmutable('now'))->days < 7)
|
||||
&& !$this->RDAPService::isToBeWatchClosely($domain, $domain->getUpdatedAt())
|
||||
&& !$this->RDAPService::isToBeWatchClosely($domain)
|
||||
&& !$this->kernel->isDebug()
|
||||
) {
|
||||
$this->logger->info('It is not necessary to update the information of the domain name {idnDomain} with the RDAP protocol.', [
|
||||
'idnDomain' => $idnDomain,
|
||||
@@ -79,7 +81,7 @@ class DomainRefreshController extends AbstractController
|
||||
|
||||
/** @var WatchList $watchList */
|
||||
foreach ($watchLists as $watchList) {
|
||||
$this->bus->dispatch(new ProcessDomainTrigger($watchList->getToken(), $domain->getLdhName(), $updatedAt));
|
||||
$this->bus->dispatch(new SendDomainEventNotif($watchList->getToken(), $domain->getLdhName(), $updatedAt));
|
||||
}
|
||||
|
||||
return $domain;
|
||||
|
||||
@@ -75,6 +75,25 @@ class RegistrationController extends AbstractController
|
||||
)
|
||||
);
|
||||
|
||||
if (false === (bool) $this->getParameter('registration_verify_email')) {
|
||||
$user->setVerified(true);
|
||||
} else {
|
||||
$email = $this->emailVerifier->sendEmailConfirmation('app_verify_email', $user,
|
||||
(new TemplatedEmail())
|
||||
->from(new Address($this->mailerSenderEmail, $this->mailerSenderName))
|
||||
->to($user->getEmail())
|
||||
->locale('en')
|
||||
->subject('Please Confirm your Email')
|
||||
->htmlTemplate('emails/success/confirmation_email.html.twig')
|
||||
);
|
||||
|
||||
$signedUrl = (string) $email->getContext()['signedUrl'];
|
||||
$this->logger->notice('The validation link for user {username} is {signedUrl}', [
|
||||
'username' => $user->getUserIdentifier(),
|
||||
'signedUrl' => $signedUrl,
|
||||
]);
|
||||
}
|
||||
|
||||
$this->em->persist($user);
|
||||
$this->em->flush();
|
||||
|
||||
@@ -82,15 +101,6 @@ class RegistrationController extends AbstractController
|
||||
'username' => $user->getUserIdentifier(),
|
||||
]);
|
||||
|
||||
$this->emailVerifier->sendEmailConfirmation('app_verify_email', $user,
|
||||
(new TemplatedEmail())
|
||||
->from(new Address($this->mailerSenderEmail, $this->mailerSenderName))
|
||||
->to($user->getEmail())
|
||||
->locale('en')
|
||||
->subject('Please Confirm your Email')
|
||||
->htmlTemplate('emails/success/confirmation_email.html.twig')
|
||||
);
|
||||
|
||||
return new Response(null, 201);
|
||||
}
|
||||
|
||||
|
||||
80
src/Controller/StatisticsController.php
Normal file
80
src/Controller/StatisticsController.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Statistics;
|
||||
use App\Repository\DomainRepository;
|
||||
use App\Repository\WatchListRepository;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
use Psr\Cache\InvalidArgumentException;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpKernel\KernelInterface;
|
||||
|
||||
class StatisticsController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly CacheItemPoolInterface $pool,
|
||||
private readonly DomainRepository $domainRepository,
|
||||
private readonly WatchListRepository $watchListRepository,
|
||||
private readonly KernelInterface $kernel
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
public function __invoke(): Statistics
|
||||
{
|
||||
$stats = new Statistics();
|
||||
|
||||
$stats
|
||||
->setRdapQueries($this->pool->getItem('stats.rdap_queries.count')->get() ?? 0)
|
||||
->setDomainPurchased($this->pool->getItem('stats.domain.purchased')->get() ?? 0)
|
||||
->setDomainPurchaseFailed($this->pool->getItem('stats.domain.purchase.failed')->get() ?? 0)
|
||||
->setAlertSent($this->pool->getItem('stats.alert.sent')->get() ?? 0)
|
||||
|
||||
->setDomainTracked(
|
||||
$this->getCachedItem('stats.domain.tracked', fn () => $this->watchListRepository->createQueryBuilder('w')
|
||||
->join('w.domains', 'd')
|
||||
->select('COUNT(DISTINCT d.ldhName)')
|
||||
->where('d.deleted = FALSE')
|
||||
->getQuery()->getSingleColumnResult()[0])
|
||||
)
|
||||
->setDomainCount(
|
||||
$this->getCachedItem('stats.domain.count', fn () => $this->domainRepository->createQueryBuilder('d')
|
||||
->join('d.tld', 't')
|
||||
->select('t.tld tld')
|
||||
->addSelect('COUNT(d.ldhName) AS domain')
|
||||
->addGroupBy('t.tld')
|
||||
->where('d.deleted = FALSE')
|
||||
->orderBy('domain', 'DESC')
|
||||
->setMaxResults(5)
|
||||
->getQuery()->getArrayResult())
|
||||
)
|
||||
->setDomainCountTotal(
|
||||
$this->getCachedItem('stats.domain.total', fn () => $this->domainRepository->count(['deleted' => false])
|
||||
));
|
||||
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
private function getCachedItem(string $key, callable $getItemFunction)
|
||||
{
|
||||
$item = $this->pool->getItem($key);
|
||||
|
||||
if (!$item->isHit() || $this->kernel->isDebug()) {
|
||||
$value = $getItemFunction();
|
||||
$item
|
||||
->set($value)
|
||||
->expiresAfter(new \DateInterval('PT6H'));
|
||||
$this->pool->save($item);
|
||||
|
||||
return $value;
|
||||
} else {
|
||||
return $item->get();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,19 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Connector;
|
||||
use App\Entity\Domain;
|
||||
use App\Entity\DomainEntity;
|
||||
use App\Entity\DomainEvent;
|
||||
use App\Entity\User;
|
||||
use App\Entity\WatchList;
|
||||
use App\Notifier\TestChatNotification;
|
||||
use App\Repository\WatchListRepository;
|
||||
use App\Service\ChatNotificationService;
|
||||
use App\Service\Connector\AbstractProvider;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Exception\ORMException;
|
||||
use Eluceo\iCal\Domain\Entity\Attendee;
|
||||
use Eluceo\iCal\Domain\Entity\Calendar;
|
||||
use Eluceo\iCal\Domain\Entity\Event;
|
||||
@@ -22,6 +27,7 @@ use Eluceo\iCal\Domain\ValueObject\Timestamp;
|
||||
use Eluceo\iCal\Presentation\Component\Property;
|
||||
use Eluceo\iCal\Presentation\Component\Property\Value\TextValue;
|
||||
use Eluceo\iCal\Presentation\Factory\CalendarFactory;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Sabre\VObject\EofException;
|
||||
use Sabre\VObject\InvalidDataException;
|
||||
@@ -31,8 +37,11 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\KernelInterface;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Serializer\SerializerInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
class WatchListController extends AbstractController
|
||||
{
|
||||
@@ -40,7 +49,11 @@ class WatchListController extends AbstractController
|
||||
private readonly SerializerInterface $serializer,
|
||||
private readonly EntityManagerInterface $em,
|
||||
private readonly WatchListRepository $watchListRepository,
|
||||
private readonly LoggerInterface $logger
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly HttpClientInterface $httpClient,
|
||||
private readonly CacheItemPoolInterface $cacheItemPool,
|
||||
private readonly KernelInterface $kernel,
|
||||
private readonly ChatNotificationService $chatNotificationService
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -69,8 +82,8 @@ class WatchListController extends AbstractController
|
||||
* This policy guarantees the equal probability of obtaining a domain name if it is requested by several users.
|
||||
*/
|
||||
if ($this->getParameter('limited_features')) {
|
||||
if ($watchList->getDomains()->count() >= (int) $this->getParameter('limit_max_watchlist_domains')) {
|
||||
$this->logger->notice('User {username} tried to create a Watchlist. However, the maximum number of domains has been reached for this Watchlist', [
|
||||
if ($watchList->getDomains()->count() > (int) $this->getParameter('limit_max_watchlist_domains')) {
|
||||
$this->logger->notice('User {username} tried to create a Watchlist. The maximum number of domains has been reached.', [
|
||||
'username' => $user->getUserIdentifier(),
|
||||
]);
|
||||
throw new AccessDeniedHttpException('You have exceeded the maximum number of domain names allowed in this Watchlist');
|
||||
@@ -78,7 +91,7 @@ class WatchListController extends AbstractController
|
||||
|
||||
$userWatchLists = $user->getWatchLists();
|
||||
if ($userWatchLists->count() >= (int) $this->getParameter('limit_max_watchlist')) {
|
||||
$this->logger->notice('User {username} tried to create a Watchlist. However, the maximum number of Watchlists has been reached.', [
|
||||
$this->logger->notice('User {username} tried to create a Watchlist. The maximum number of Watchlists has been reached', [
|
||||
'username' => $user->getUserIdentifier(),
|
||||
]);
|
||||
throw new AccessDeniedHttpException('You have exceeded the maximum number of Watchlists allowed');
|
||||
@@ -90,17 +103,29 @@ class WatchListController extends AbstractController
|
||||
/** @var Domain $domain */
|
||||
foreach ($watchList->getDomains()->getIterator() as $domain) {
|
||||
if (in_array($domain, $trackedDomains)) {
|
||||
$this->logger->notice('User {username} tried to create a watchlist with domain name {ldhName}. However, it is forbidden to register the same domain name twice with limited mode.', [
|
||||
$ldhName = $domain->getLdhName();
|
||||
|
||||
$this->logger->notice('User {username} tried to create a watchlist with domain name {ldhName}. It is forbidden to register the same domain name twice with limited mode', [
|
||||
'username' => $user->getUserIdentifier(),
|
||||
'ldhName' => $domain->getLdhName(),
|
||||
'ldhName' => $ldhName,
|
||||
]);
|
||||
|
||||
throw new AccessDeniedHttpException('It is forbidden to register the same domain name twice in your watchlists with limited mode.');
|
||||
throw new AccessDeniedHttpException("It is forbidden to register the same domain name twice in your watchlists with limited mode ($ldhName)");
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== $watchList->getWebhookDsn() && count($watchList->getWebhookDsn()) > (int) $this->getParameter('limit_max_watchlist_webhooks')) {
|
||||
$this->logger->notice('User {username} tried to create a Watchlist. The maximum number of webhooks has been reached.', [
|
||||
'username' => $user->getUserIdentifier(),
|
||||
]);
|
||||
throw new AccessDeniedHttpException('You have exceeded the maximum number of webhooks allowed in this Watchlist');
|
||||
}
|
||||
}
|
||||
|
||||
$this->logger->info('User {username} register a Watchlist ({token}).', [
|
||||
$this->chatNotificationService->sendChatNotification($watchList, new TestChatNotification());
|
||||
$this->verifyConnector($watchList, $watchList->getConnector());
|
||||
|
||||
$this->logger->info('User {username} registers a Watchlist ({token}).', [
|
||||
'username' => $user->getUserIdentifier(),
|
||||
'token' => $watchList->getToken(),
|
||||
]);
|
||||
@@ -111,6 +136,138 @@ class WatchListController extends AbstractController
|
||||
return $watchList;
|
||||
}
|
||||
|
||||
#[Route(
|
||||
path: '/api/watchlists',
|
||||
name: 'watchlist_get_all_mine',
|
||||
defaults: [
|
||||
'_api_resource_class' => WatchList::class,
|
||||
'_api_operation_name' => 'get_all_mine',
|
||||
],
|
||||
methods: ['GET']
|
||||
)]
|
||||
public function getWatchLists(): Collection
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
|
||||
return $user->getWatchLists();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function verifyConnector(WatchList $watchList, ?Connector $connector): void
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
|
||||
if (null === $connector) {
|
||||
return;
|
||||
}
|
||||
if (!$user->getConnectors()->contains($connector)) {
|
||||
$this->logger->notice('The Connector ({connector}) does not belong to the user.', [
|
||||
'username' => $user->getUserIdentifier(),
|
||||
'connector' => $connector->getId(),
|
||||
]);
|
||||
throw new AccessDeniedHttpException('You cannot create a Watchlist with a connector that does not belong to you');
|
||||
}
|
||||
|
||||
/** @var Domain $domain */
|
||||
foreach ($watchList->getDomains()->getIterator() as $domain) {
|
||||
if ($domain->getDeleted()) {
|
||||
$ldhName = $domain->getLdhName();
|
||||
throw new BadRequestHttpException("To add a connector, no domain in this Watchlist must have already expired ($ldhName)");
|
||||
}
|
||||
}
|
||||
|
||||
$connectorProviderClass = $connector->getProvider()->getConnectorProvider();
|
||||
/** @var AbstractProvider $connectorProvider */
|
||||
$connectorProvider = new $connectorProviderClass($connector->getAuthData(), $this->httpClient, $this->cacheItemPool, $this->kernel);
|
||||
|
||||
$connectorProvider::verifyAuthData($connector->getAuthData(), $this->httpClient); // We want to check if the tokens are OK
|
||||
$supported = $connectorProvider->isSupported(...$watchList->getDomains()->toArray());
|
||||
|
||||
if (!$supported) {
|
||||
$this->logger->notice('The Connector ({connector}) does not support all TLDs in this Watchlist', [
|
||||
'username' => $user->getUserIdentifier(),
|
||||
'connector' => $connector->getId(),
|
||||
]);
|
||||
throw new BadRequestHttpException('This connector does not support all TLDs in this Watchlist');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ORMException
|
||||
* @throws \Exception
|
||||
*/
|
||||
#[Route(
|
||||
path: '/api/watchlists/{token}',
|
||||
name: 'watchlist_update',
|
||||
defaults: [
|
||||
'_api_resource_class' => WatchList::class,
|
||||
'_api_operation_name' => 'update',
|
||||
],
|
||||
methods: ['PUT']
|
||||
)]
|
||||
public function putWatchList(WatchList $watchList): WatchList
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
$watchList->setUser($user);
|
||||
|
||||
if ($this->getParameter('limited_features')) {
|
||||
if ($watchList->getDomains()->count() > (int) $this->getParameter('limit_max_watchlist_domains')) {
|
||||
$this->logger->notice('User {username} tried to update a Watchlist. The maximum number of domains has been reached for this Watchlist', [
|
||||
'username' => $user->getUserIdentifier(),
|
||||
]);
|
||||
throw new AccessDeniedHttpException('You have exceeded the maximum number of domain names allowed in this Watchlist');
|
||||
}
|
||||
|
||||
$userWatchLists = $user->getWatchLists();
|
||||
|
||||
/** @var Domain[] $trackedDomains */
|
||||
$trackedDomains = $userWatchLists
|
||||
->filter(fn (WatchList $wl) => $wl->getToken() !== $watchList->getToken())
|
||||
->reduce(fn (array $acc, WatchList $wl) => [...$acc, ...$wl->getDomains()->toArray()], []);
|
||||
|
||||
/** @var Domain $domain */
|
||||
foreach ($watchList->getDomains()->getIterator() as $domain) {
|
||||
if (in_array($domain, $trackedDomains)) {
|
||||
$ldhName = $domain->getLdhName();
|
||||
$this->logger->notice('User {username} tried to update a watchlist with domain name {ldhName}. It is forbidden to register the same domain name twice with limited mode', [
|
||||
'username' => $user->getUserIdentifier(),
|
||||
'ldhName' => $ldhName,
|
||||
]);
|
||||
|
||||
throw new AccessDeniedHttpException("It is forbidden to register the same domain name twice in your watchlists with limited mode ($ldhName)");
|
||||
}
|
||||
}
|
||||
|
||||
if (null !== $watchList->getWebhookDsn() && count($watchList->getWebhookDsn()) > (int) $this->getParameter('limit_max_watchlist_webhooks')) {
|
||||
$this->logger->notice('User {username} tried to update a Watchlist. The maximum number of webhooks has been reached.', [
|
||||
'username' => $user->getUserIdentifier(),
|
||||
]);
|
||||
throw new AccessDeniedHttpException('You have exceeded the maximum number of webhooks allowed in this Watchlist');
|
||||
}
|
||||
}
|
||||
|
||||
$this->chatNotificationService->sendChatNotification($watchList, new TestChatNotification());
|
||||
$this->verifyConnector($watchList, $watchList->getConnector());
|
||||
|
||||
$this->logger->info('User {username} updates a Watchlist ({token}).', [
|
||||
'username' => $user->getUserIdentifier(),
|
||||
'token' => $watchList->getToken(),
|
||||
]);
|
||||
|
||||
$this->em->remove($this->em->getReference(WatchList::class, $watchList->getToken()));
|
||||
$this->em->flush();
|
||||
|
||||
$this->em->persist($watchList);
|
||||
$this->em->flush();
|
||||
|
||||
return $watchList;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ParseException
|
||||
* @throws EofException
|
||||
@@ -149,7 +306,7 @@ class WatchListController extends AbstractController
|
||||
}
|
||||
|
||||
/** @var DomainEvent $event */
|
||||
foreach ($domain->getEvents()->toArray() as $event) {
|
||||
foreach ($domain->getEvents()->filter(fn (DomainEvent $e) => $e->getDate()->diff(new \DateTimeImmutable('now'))->y <= 10)->getIterator() as $event) {
|
||||
$calendar->addEvent((new Event())
|
||||
->setLastModified(new Timestamp($domain->getUpdatedAt()))
|
||||
->setStatus(EventStatus::CONFIRMED())
|
||||
@@ -172,20 +329,55 @@ class WatchListController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Exception
|
||||
*/
|
||||
#[Route(
|
||||
path: '/api/watchlists',
|
||||
name: 'watchlist_get_all_mine',
|
||||
path: '/api/tracked',
|
||||
name: 'watchlist_get_tracked_domains',
|
||||
defaults: [
|
||||
'_api_resource_class' => WatchList::class,
|
||||
'_api_operation_name' => 'get_all_mine',
|
||||
],
|
||||
methods: ['GET']
|
||||
'_api_operation_name' => 'get_tracked_domains',
|
||||
]
|
||||
)]
|
||||
public function getWatchLists(): Collection
|
||||
public function getTrackedDomains(): array
|
||||
{
|
||||
/** @var User $user */
|
||||
$user = $this->getUser();
|
||||
|
||||
return $user->getWatchLists();
|
||||
$domains = [];
|
||||
/** @var WatchList $watchList */
|
||||
foreach ($user->getWatchLists()->getIterator() as $watchList) {
|
||||
/** @var Domain $domain */
|
||||
foreach ($watchList->getDomains()->getIterator() as $domain) {
|
||||
/** @var DomainEvent|null $exp */
|
||||
$exp = $domain->getEvents()->findFirst(fn (int $key, DomainEvent $e) => !$e->getDeleted() && 'expiration' === $e->getAction());
|
||||
|
||||
if (!$domain->getDeleted()
|
||||
&& null !== $exp && $exp->getDate() > new \DateTimeImmutable()
|
||||
&& count(array_filter($domain->getEvents()->toArray(), fn (DomainEvent $e) => !$e->getDeleted() && 'expiration' === $e->getAction())) > 0
|
||||
&& !in_array($domain, $domains)) {
|
||||
$domains[] = $domain;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
usort($domains, function (Domain $d1, Domain $d2) {
|
||||
$IMPORTANT_STATUS = ['pending delete', 'redemption period', 'auto renew period'];
|
||||
|
||||
/** @var \DateTimeImmutable $exp1 */
|
||||
$exp1 = $d1->getEvents()->findFirst(fn (int $key, DomainEvent $e) => !$e->getDeleted() && 'expiration' === $e->getAction())->getDate();
|
||||
/** @var \DateTimeImmutable $exp2 */
|
||||
$exp2 = $d2->getEvents()->findFirst(fn (int $key, DomainEvent $e) => !$e->getDeleted() && 'expiration' === $e->getAction())->getDate();
|
||||
$impStatus1 = count(array_intersect($IMPORTANT_STATUS, $d1->getStatus())) > 0;
|
||||
$impStatus2 = count(array_intersect($IMPORTANT_STATUS, $d2->getStatus())) > 0;
|
||||
|
||||
return $impStatus1 && !$impStatus2 ? -1 : (
|
||||
!$impStatus1 && $impStatus2 ? 2 :
|
||||
$exp1 <=> $exp2
|
||||
);
|
||||
});
|
||||
|
||||
return $domains;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,9 @@ use Symfony\Component\Uid\Uuid;
|
||||
normalizationContext: ['groups' => ['connector:create', 'connector:list']], denormalizationContext: ['groups' => 'connector:create'],
|
||||
name: 'create'
|
||||
),
|
||||
new Delete(),
|
||||
new Delete(
|
||||
security: 'object.user == user'
|
||||
),
|
||||
]
|
||||
)]
|
||||
#[ORM\Entity(repositoryClass: ConnectorRepository::class)]
|
||||
|
||||
@@ -57,7 +57,7 @@ class Domain
|
||||
* @var Collection<int, DomainEvent>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: DomainEvent::class, mappedBy: 'domain', cascade: ['persist'], orphanRemoval: true)]
|
||||
#[Groups(['domain:item'])]
|
||||
#[Groups(['domain:item', 'domain:list'])]
|
||||
private Collection $events;
|
||||
|
||||
/**
|
||||
@@ -69,7 +69,7 @@ class Domain
|
||||
private Collection $domainEntities;
|
||||
|
||||
#[ORM\Column(type: Types::SIMPLE_ARRAY, nullable: true)]
|
||||
#[Groups(['domain:item'])]
|
||||
#[Groups(['domain:item', 'domain:list'])]
|
||||
private array $status = [];
|
||||
|
||||
/**
|
||||
@@ -90,19 +90,20 @@ class Domain
|
||||
private Collection $nameservers;
|
||||
|
||||
#[ORM\Column(type: Types::DATE_IMMUTABLE)]
|
||||
private ?\DateTimeImmutable $createdAt = null;
|
||||
private ?\DateTimeImmutable $createdAt;
|
||||
|
||||
#[ORM\Column(type: Types::DATE_IMMUTABLE)]
|
||||
private ?\DateTimeImmutable $updatedAt = null;
|
||||
#[ORM\Column(type: Types::DATETIME_IMMUTABLE)]
|
||||
#[Groups(['domain:item', 'domain:list'])]
|
||||
private ?\DateTimeImmutable $updatedAt;
|
||||
|
||||
#[ORM\ManyToOne]
|
||||
#[ORM\JoinColumn(referencedColumnName: 'tld', nullable: false)]
|
||||
#[Groups(['domain:item'])]
|
||||
#[Groups(['domain:item', 'domain:list'])]
|
||||
private ?Tld $tld = null;
|
||||
|
||||
#[ORM\Column]
|
||||
#[Groups(['domain:item'])]
|
||||
private ?bool $deleted = null;
|
||||
#[ORM\Column(nullable: false)]
|
||||
#[Groups(['domain:item', 'domain:list'])]
|
||||
private ?bool $deleted;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
|
||||
@@ -27,13 +27,13 @@ class DomainEntity
|
||||
#[Groups(['domain-entity:entity', 'domain-entity:domain'])]
|
||||
private array $roles = [];
|
||||
|
||||
#[ORM\Column(type: Types::DATE_IMMUTABLE)]
|
||||
#[ORM\Column]
|
||||
#[Groups(['domain-entity:entity', 'domain-entity:domain'])]
|
||||
private ?\DateTimeImmutable $updatedAt = null;
|
||||
private ?bool $deleted;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->updatedAt = new \DateTimeImmutable('now');
|
||||
$this->deleted = false;
|
||||
}
|
||||
|
||||
public function getDomain(): ?Domain
|
||||
@@ -75,22 +75,15 @@ class DomainEntity
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUpdatedAt(): ?\DateTimeImmutable
|
||||
public function getDeleted(): ?bool
|
||||
{
|
||||
return $this->updatedAt;
|
||||
return $this->deleted;
|
||||
}
|
||||
|
||||
public function setUpdatedAt(\DateTimeImmutable $updatedAt): static
|
||||
public function setDeleted(?bool $deleted): static
|
||||
{
|
||||
$this->updatedAt = $updatedAt;
|
||||
$this->deleted = $deleted;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
#[ORM\PrePersist]
|
||||
#[ORM\PreUpdate]
|
||||
public function updateTimestamps(): void
|
||||
{
|
||||
$this->setUpdatedAt(new \DateTimeImmutable('now'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
#[ORM\Entity(repositoryClass: DomainEventRepository::class)]
|
||||
class DomainEvent extends Event
|
||||
{
|
||||
#[ORM\ManyToOne(targetEntity: Domain::class, cascade: ['persist'], inversedBy: 'events')]
|
||||
#[ORM\ManyToOne(targetEntity: Domain::class, inversedBy: 'events')]
|
||||
#[ORM\JoinColumn(referencedColumnName: 'ldh_name', nullable: false)]
|
||||
private ?Domain $domain = null;
|
||||
|
||||
|
||||
@@ -21,6 +21,15 @@ class Event
|
||||
#[Groups(['event:list'])]
|
||||
private ?\DateTimeImmutable $date = null;
|
||||
|
||||
#[ORM\Column]
|
||||
#[Groups(['event:list'])]
|
||||
private ?bool $deleted;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->deleted = false;
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
@@ -49,4 +58,16 @@ class Event
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDeleted(): ?bool
|
||||
{
|
||||
return $this->deleted;
|
||||
}
|
||||
|
||||
public function setDeleted(?bool $deleted): static
|
||||
{
|
||||
$this->deleted = $deleted;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
111
src/Entity/Statistics.php
Normal file
111
src/Entity/Statistics.php
Normal file
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use App\Controller\StatisticsController;
|
||||
|
||||
#[ApiResource(
|
||||
operations: [
|
||||
new Get(
|
||||
uriTemplate: '/stats',
|
||||
controller: StatisticsController::class,
|
||||
shortName: 'Statistics',
|
||||
read: false,
|
||||
),
|
||||
]
|
||||
)]
|
||||
class Statistics
|
||||
{
|
||||
private ?int $rdapQueries = null;
|
||||
private ?int $alertSent = null;
|
||||
private ?int $domainPurchased = null;
|
||||
private ?int $domainPurchaseFailed = null;
|
||||
|
||||
private ?array $domainCount = null;
|
||||
private ?int $domainTracked = null;
|
||||
private ?int $domainCountTotal = null;
|
||||
|
||||
public function getRdapQueries(): ?int
|
||||
{
|
||||
return $this->rdapQueries;
|
||||
}
|
||||
|
||||
public function setRdapQueries(?int $rdapQueries): static
|
||||
{
|
||||
$this->rdapQueries = $rdapQueries;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAlertSent(): ?int
|
||||
{
|
||||
return $this->alertSent;
|
||||
}
|
||||
|
||||
public function setAlertSent(?int $alertSent): static
|
||||
{
|
||||
$this->alertSent = $alertSent;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDomainPurchased(): ?int
|
||||
{
|
||||
return $this->domainPurchased;
|
||||
}
|
||||
|
||||
public function setDomainPurchased(?int $domainPurchased): static
|
||||
{
|
||||
$this->domainPurchased = $domainPurchased;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDomainCount(): ?array
|
||||
{
|
||||
return $this->domainCount;
|
||||
}
|
||||
|
||||
public function setDomainCount(?array $domainCount): static
|
||||
{
|
||||
$this->domainCount = $domainCount;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDomainCountTotal(): ?int
|
||||
{
|
||||
return $this->domainCountTotal;
|
||||
}
|
||||
|
||||
public function setDomainCountTotal(?int $domainCountTotal): void
|
||||
{
|
||||
$this->domainCountTotal = $domainCountTotal;
|
||||
}
|
||||
|
||||
public function getDomainPurchaseFailed(): ?int
|
||||
{
|
||||
return $this->domainPurchaseFailed;
|
||||
}
|
||||
|
||||
public function setDomainPurchaseFailed(?int $domainPurchaseFailed): static
|
||||
{
|
||||
$this->domainPurchaseFailed = $domainPurchaseFailed;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getDomainTracked(): ?int
|
||||
{
|
||||
return $this->domainTracked;
|
||||
}
|
||||
|
||||
public function setDomainTracked(?int $domainTracked): static
|
||||
{
|
||||
$this->domainTracked = $domainTracked;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
@@ -6,12 +6,12 @@ use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use App\Controller\WatchListController;
|
||||
use ApiPlatform\Metadata\Put;
|
||||
use App\Repository\WatchListRepository;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
use Symfony\Component\Serializer\Attribute\SerializedName;
|
||||
@@ -26,13 +26,31 @@ use Symfony\Component\Uid\Uuid;
|
||||
normalizationContext: ['groups' => 'watchlist:list'],
|
||||
name: 'get_all_mine',
|
||||
),
|
||||
new GetCollection(
|
||||
uriTemplate: '/tracked',
|
||||
routeName: 'watchlist_get_tracked_domains',
|
||||
normalizationContext: ['groups' => [
|
||||
'domain:list',
|
||||
'tld:list',
|
||||
'event:list',
|
||||
]],
|
||||
name: 'get_tracked_domains'
|
||||
),
|
||||
new Get(
|
||||
normalizationContext: ['groups' => 'watchlist:item'],
|
||||
normalizationContext: ['groups' => [
|
||||
'watchlist:item',
|
||||
'domain:item',
|
||||
'event:list',
|
||||
'domain-entity:entity',
|
||||
'nameserver-entity:nameserver',
|
||||
'nameserver-entity:entity',
|
||||
'tld:item',
|
||||
],
|
||||
],
|
||||
security: 'object.user == user'
|
||||
),
|
||||
new Get(
|
||||
routeName: 'watchlist_calendar',
|
||||
controller: WatchListController::class,
|
||||
openapiContext: [
|
||||
'responses' => [
|
||||
'200' => [
|
||||
@@ -58,24 +76,27 @@ use Symfony\Component\Uid\Uuid;
|
||||
denormalizationContext: ['groups' => 'watchlist:create'],
|
||||
name: 'create'
|
||||
),
|
||||
new Patch(
|
||||
new Put(
|
||||
routeName: 'watchlist_update',
|
||||
normalizationContext: ['groups' => 'watchlist:item'],
|
||||
denormalizationContext: ['groups' => 'watchlist:update']
|
||||
denormalizationContext: ['groups' => ['watchlist:create', 'watchlist:token']],
|
||||
security: 'object.user == user',
|
||||
name: 'update'
|
||||
),
|
||||
new Delete(
|
||||
security: 'object.user == user'
|
||||
),
|
||||
new Delete(),
|
||||
],
|
||||
)]
|
||||
class WatchList
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: 'uuid')]
|
||||
#[Groups(['watchlist:item', 'watchlist:list'])]
|
||||
private string $token;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: User::class, inversedBy: 'watchLists')]
|
||||
#[ORM\JoinColumn(nullable: false, onDelete: 'CASCADE')]
|
||||
public ?User $user = null;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: 'uuid')]
|
||||
#[Groups(['watchlist:item', 'watchlist:list', 'watchlist:token'])]
|
||||
private string $token;
|
||||
/**
|
||||
* @var Collection<int, Domain>
|
||||
*/
|
||||
@@ -83,29 +104,34 @@ class WatchList
|
||||
#[ORM\JoinTable(name: 'watch_lists_domains',
|
||||
joinColumns: [new ORM\JoinColumn(name: 'watch_list_token', referencedColumnName: 'token', onDelete: 'CASCADE')],
|
||||
inverseJoinColumns: [new ORM\JoinColumn(name: 'domain_ldh_name', referencedColumnName: 'ldh_name', onDelete: 'CASCADE')])]
|
||||
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create', 'watchlist:update'])]
|
||||
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create'])]
|
||||
private Collection $domains;
|
||||
|
||||
/**
|
||||
* @var Collection<int, WatchListTrigger>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: WatchListTrigger::class, mappedBy: 'watchList', cascade: ['persist'], orphanRemoval: true)]
|
||||
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create', 'watchlist:update'])]
|
||||
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create'])]
|
||||
#[SerializedName('triggers')]
|
||||
private Collection $watchListTriggers;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'watchLists')]
|
||||
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create', 'watchlist:update'])]
|
||||
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create'])]
|
||||
private ?Connector $connector = null;
|
||||
|
||||
#[ORM\Column(length: 255, nullable: true)]
|
||||
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create', 'watchlist:update'])]
|
||||
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create'])]
|
||||
private ?string $name = null;
|
||||
|
||||
#[ORM\Column]
|
||||
#[Groups(['watchlist:list', 'watchlist:item'])]
|
||||
private ?\DateTimeImmutable $createdAt = null;
|
||||
|
||||
#[SerializedName('dsn')]
|
||||
#[ORM\Column(type: Types::SIMPLE_ARRAY, nullable: true)]
|
||||
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create'])]
|
||||
private ?array $webhookDsn = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->token = Uuid::v4();
|
||||
@@ -119,6 +145,11 @@ class WatchList
|
||||
return $this->token;
|
||||
}
|
||||
|
||||
public function setToken(string $token): void
|
||||
{
|
||||
$this->token = $token;
|
||||
}
|
||||
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
@@ -220,4 +251,16 @@ class WatchList
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getWebhookDsn(): ?array
|
||||
{
|
||||
return $this->webhookDsn;
|
||||
}
|
||||
|
||||
public function setWebhookDsn(?array $webhookDsn): static
|
||||
{
|
||||
$this->webhookDsn = $webhookDsn;
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,17 +12,17 @@ class WatchListTrigger
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(length: 255)]
|
||||
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create', 'watchlist:update'])]
|
||||
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create'])]
|
||||
private ?string $event = null;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\ManyToOne(targetEntity: WatchList::class, inversedBy: 'watchListTriggers')]
|
||||
#[ORM\ManyToOne(targetEntity: WatchList::class, cascade: ['persist'], inversedBy: 'watchListTriggers')]
|
||||
#[ORM\JoinColumn(referencedColumnName: 'token', nullable: false, onDelete: 'CASCADE')]
|
||||
private ?WatchList $watchList = null;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(enumType: TriggerAction::class)]
|
||||
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create', 'watchlist:update'])]
|
||||
#[Groups(['watchlist:list', 'watchlist:item', 'watchlist:create'])]
|
||||
private ?TriggerAction $action = null;
|
||||
|
||||
public function getEvent(): ?string
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Message;
|
||||
|
||||
final class ProcessDomainTrigger
|
||||
final class OrderDomain
|
||||
{
|
||||
public function __construct(
|
||||
public string $watchListToken,
|
||||
13
src/Message/SendDomainEventNotif.php
Normal file
13
src/Message/SendDomainEventNotif.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App\Message;
|
||||
|
||||
final class SendDomainEventNotif
|
||||
{
|
||||
public function __construct(
|
||||
public string $watchListToken,
|
||||
public string $ldhName,
|
||||
public \DateTimeImmutable $updatedAt
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace App\Message;
|
||||
|
||||
final readonly class ProcessWatchListTrigger
|
||||
final readonly class UpdateDomainsFromWatchlist
|
||||
{
|
||||
public function __construct(
|
||||
public string $watchListToken,
|
||||
101
src/MessageHandler/OrderDomainHandler.php
Normal file
101
src/MessageHandler/OrderDomainHandler.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace App\MessageHandler;
|
||||
|
||||
use App\Entity\Domain;
|
||||
use App\Entity\WatchList;
|
||||
use App\Message\OrderDomain;
|
||||
use App\Notifier\DomainOrderErrorNotification;
|
||||
use App\Notifier\DomainOrderNotification;
|
||||
use App\Repository\DomainRepository;
|
||||
use App\Repository\WatchListRepository;
|
||||
use App\Service\ChatNotificationService;
|
||||
use App\Service\Connector\AbstractProvider;
|
||||
use App\Service\StatService;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpKernel\KernelInterface;
|
||||
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||
use Symfony\Component\Mailer\MailerInterface;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
use Symfony\Component\Mime\Address;
|
||||
use Symfony\Component\Notifier\Recipient\Recipient;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
#[AsMessageHandler]
|
||||
final readonly class OrderDomainHandler
|
||||
{
|
||||
private Address $sender;
|
||||
|
||||
public function __construct(
|
||||
string $mailerSenderEmail,
|
||||
string $mailerSenderName,
|
||||
private WatchListRepository $watchListRepository,
|
||||
private DomainRepository $domainRepository,
|
||||
private KernelInterface $kernel,
|
||||
private HttpClientInterface $client,
|
||||
private CacheItemPoolInterface $cacheItemPool,
|
||||
private MailerInterface $mailer,
|
||||
private LoggerInterface $logger,
|
||||
private StatService $statService,
|
||||
private ChatNotificationService $chatNotificationService,
|
||||
#[Autowire(service: 'service_container')]
|
||||
private ContainerInterface $locator
|
||||
) {
|
||||
$this->sender = new Address($mailerSenderEmail, $mailerSenderName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws TransportExceptionInterface
|
||||
* @throws \Symfony\Component\Notifier\Exception\TransportExceptionInterface
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function __invoke(OrderDomain $message): void
|
||||
{
|
||||
/** @var WatchList $watchList */
|
||||
$watchList = $this->watchListRepository->findOneBy(['token' => $message->watchListToken]);
|
||||
/** @var Domain $domain */
|
||||
$domain = $this->domainRepository->findOneBy(['ldhName' => $message->ldhName]);
|
||||
|
||||
$connector = $watchList->getConnector();
|
||||
if (null !== $connector && $domain->getDeleted()) {
|
||||
$this->logger->notice('Watchlist {watchlist} is linked to connector {connector}. A purchase attempt will be made for domain name {ldhName} with provider {provider}.', [
|
||||
'watchlist' => $message->watchListToken,
|
||||
'connector' => $connector->getId(),
|
||||
'ldhName' => $message->ldhName,
|
||||
'provider' => $connector->getProvider()->value,
|
||||
]);
|
||||
try {
|
||||
$provider = $connector->getProvider();
|
||||
if (null === $provider) {
|
||||
throw new \InvalidArgumentException('Provider not found');
|
||||
}
|
||||
|
||||
$connectorProviderClass = $provider->getConnectorProvider();
|
||||
|
||||
/** @var AbstractProvider $connectorProvider */
|
||||
$connectorProvider = $this->locator->get($connectorProviderClass);
|
||||
$connectorProvider->authenticate($connector->getAuthData());
|
||||
|
||||
$connectorProvider->orderDomain($domain, $this->kernel->isDebug());
|
||||
$this->statService->incrementStat('stats.domain.purchased');
|
||||
|
||||
$notification = (new DomainOrderNotification($this->sender, $domain, $connector));
|
||||
$this->mailer->send($notification->asEmailMessage(new Recipient($watchList->getUser()->getEmail()))->getMessage());
|
||||
$this->chatNotificationService->sendChatNotification($watchList, $notification);
|
||||
} catch (\Throwable $exception) {
|
||||
$this->logger->warning('Unable to complete purchase. An error message is sent to user {username}.', [
|
||||
'username' => $watchList->getUser()->getUserIdentifier(),
|
||||
]);
|
||||
|
||||
$notification = (new DomainOrderErrorNotification($this->sender, $domain));
|
||||
$this->mailer->send($notification->asEmailMessage(new Recipient($watchList->getUser()->getEmail()))->getMessage());
|
||||
$this->chatNotificationService->sendChatNotification($watchList, $notification);
|
||||
|
||||
$this->statService->incrementStat('stats.domain.purchase.failed');
|
||||
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,164 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\MessageHandler;
|
||||
|
||||
use App\Config\TriggerAction;
|
||||
use App\Entity\Connector;
|
||||
use App\Entity\Domain;
|
||||
use App\Entity\DomainEvent;
|
||||
use App\Entity\User;
|
||||
use App\Entity\WatchList;
|
||||
use App\Entity\WatchListTrigger;
|
||||
use App\Message\ProcessDomainTrigger;
|
||||
use App\Repository\DomainRepository;
|
||||
use App\Repository\WatchListRepository;
|
||||
use App\Service\Connector\ConnectorInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\HttpKernel\KernelInterface;
|
||||
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||
use Symfony\Component\Mailer\MailerInterface;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
use Symfony\Component\Mime\Address;
|
||||
use Symfony\Component\Mime\Email;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
#[AsMessageHandler]
|
||||
final readonly class ProcessDomainTriggerHandler
|
||||
{
|
||||
public function __construct(
|
||||
private string $mailerSenderEmail,
|
||||
private string $mailerSenderName,
|
||||
private MailerInterface $mailer,
|
||||
private WatchListRepository $watchListRepository,
|
||||
private DomainRepository $domainRepository,
|
||||
private KernelInterface $kernel,
|
||||
private LoggerInterface $logger,
|
||||
private HttpClientInterface $client,
|
||||
#[Autowire(service: 'service_container')]
|
||||
private ContainerInterface $locator
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws TransportExceptionInterface
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function __invoke(ProcessDomainTrigger $message): void
|
||||
{
|
||||
/** @var WatchList $watchList */
|
||||
$watchList = $this->watchListRepository->findOneBy(['token' => $message->watchListToken]);
|
||||
/** @var Domain $domain */
|
||||
$domain = $this->domainRepository->findOneBy(['ldhName' => $message->ldhName]);
|
||||
|
||||
$connector = $watchList->getConnector();
|
||||
if (null !== $connector && $domain->getDeleted()) {
|
||||
$this->logger->notice('Watchlist {watchlist} is linked to connector {connector}. A purchase attempt will be made for domain name {ldhName} with provider {provider}.', [
|
||||
'watchlist' => $message->watchListToken,
|
||||
'connector' => $connector->getId(),
|
||||
'ldhName' => $message->ldhName,
|
||||
'provider' => $connector->getProvider()->value,
|
||||
]);
|
||||
try {
|
||||
$provider = $connector->getProvider();
|
||||
if (null === $provider) {
|
||||
throw new \Exception('Provider not found');
|
||||
}
|
||||
|
||||
$connectorProviderClass = $provider->getConnectorProvider();
|
||||
|
||||
/** @var ConnectorInterface $connectorProvider */
|
||||
$connectorProvider = $this->locator->get($connectorProviderClass);
|
||||
|
||||
$connectorProvider->authenticate($connector->getAuthData());
|
||||
$connectorProvider->orderDomain($domain, /* $this->kernel->isDebug() */ false);
|
||||
|
||||
$this->sendEmailDomainOrdered($domain, $connector, $watchList->getUser());
|
||||
} catch (\Throwable $t) {
|
||||
dump($t);
|
||||
$this->logger->error('Unable to complete purchase. An error message is sent to user {username}.', [
|
||||
'username' => $watchList->getUser()->getUserIdentifier(),
|
||||
'error' => $t,
|
||||
]);
|
||||
$this->sendEmailDomainOrderError($domain, $watchList->getUser());
|
||||
}
|
||||
}
|
||||
|
||||
/** @var DomainEvent $event */
|
||||
foreach ($domain->getEvents()->filter(fn ($event) => $message->updatedAt < $event->getDate() && $event->getDate() < new \DateTime()) as $event) {
|
||||
$watchListTriggers = $watchList->getWatchListTriggers()
|
||||
->filter(fn ($trigger) => $trigger->getEvent() === $event->getAction());
|
||||
|
||||
/** @var WatchListTrigger $watchListTrigger */
|
||||
foreach ($watchListTriggers->getIterator() as $watchListTrigger) {
|
||||
$this->logger->info('Action {event} has been detected on the domain name {ldhName}. A notification is sent to user {username}.', [
|
||||
'event' => $event->getAction(),
|
||||
'ldhName' => $message->ldhName,
|
||||
'username' => $watchList->getUser()->getUserIdentifier(),
|
||||
]);
|
||||
if (TriggerAction::SendEmail == $watchListTrigger->getAction()) {
|
||||
$this->sendEmailDomainUpdated($event, $watchList->getUser());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws TransportExceptionInterface
|
||||
*/
|
||||
private function sendEmailDomainOrdered(Domain $domain, Connector $connector, User $user): void
|
||||
{
|
||||
$email = (new TemplatedEmail())
|
||||
->from(new Address($this->mailerSenderEmail, $this->mailerSenderName))
|
||||
->to($user->getEmail())
|
||||
->priority(Email::PRIORITY_HIGHEST)
|
||||
->subject('A domain name has been ordered')
|
||||
->htmlTemplate('emails/success/domain_ordered.html.twig')
|
||||
->locale('en')
|
||||
->context([
|
||||
'domain' => $domain,
|
||||
'provider' => $connector->getProvider()->value,
|
||||
]);
|
||||
|
||||
$this->mailer->send($email);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws TransportExceptionInterface
|
||||
*/
|
||||
private function sendEmailDomainOrderError(Domain $domain, User $user): void
|
||||
{
|
||||
$email = (new TemplatedEmail())
|
||||
->from(new Address($this->mailerSenderEmail, $this->mailerSenderName))
|
||||
->to($user->getEmail())
|
||||
->subject('An error occurred while ordering a domain name')
|
||||
->htmlTemplate('emails/errors/domain_order.html.twig')
|
||||
->locale('en')
|
||||
->context([
|
||||
'domain' => $domain,
|
||||
]);
|
||||
|
||||
$this->mailer->send($email);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws TransportExceptionInterface
|
||||
*/
|
||||
private function sendEmailDomainUpdated(DomainEvent $domainEvent, User $user): void
|
||||
{
|
||||
$email = (new TemplatedEmail())
|
||||
->from(new Address($this->mailerSenderEmail, $this->mailerSenderName))
|
||||
->to($user->getEmail())
|
||||
->priority(Email::PRIORITY_HIGHEST)
|
||||
->subject('A domain name has been changed')
|
||||
->htmlTemplate('emails/success/domain_updated.html.twig')
|
||||
->locale('en')
|
||||
->context([
|
||||
'event' => $domainEvent,
|
||||
]);
|
||||
|
||||
$this->mailer->send($email);
|
||||
}
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\MessageHandler;
|
||||
|
||||
use App\Entity\Domain;
|
||||
use App\Entity\User;
|
||||
use App\Entity\WatchList;
|
||||
use App\Message\ProcessDomainTrigger;
|
||||
use App\Message\ProcessWatchListTrigger;
|
||||
use App\Repository\WatchListRepository;
|
||||
use App\Service\RDAPService;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||
use Symfony\Component\Mailer\MailerInterface;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
use Symfony\Component\Messenger\Exception\ExceptionInterface;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Component\Mime\Address;
|
||||
|
||||
#[AsMessageHandler]
|
||||
final readonly class ProcessWatchListTriggerHandler
|
||||
{
|
||||
public function __construct(
|
||||
private RDAPService $RDAPService,
|
||||
private MailerInterface $mailer,
|
||||
private string $mailerSenderEmail,
|
||||
private string $mailerSenderName,
|
||||
private MessageBusInterface $bus,
|
||||
private WatchListRepository $watchListRepository,
|
||||
private LoggerInterface $logger
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws TransportExceptionInterface
|
||||
* @throws \Exception
|
||||
* @throws ExceptionInterface
|
||||
*/
|
||||
public function __invoke(ProcessWatchListTrigger $message): void
|
||||
{
|
||||
/** @var WatchList $watchList */
|
||||
$watchList = $this->watchListRepository->findOneBy(['token' => $message->watchListToken]);
|
||||
|
||||
$this->logger->info('Domain names from Watchlist {token} will be processed.', [
|
||||
'token' => $message->watchListToken,
|
||||
]);
|
||||
|
||||
/** @var Domain $domain */
|
||||
foreach ($watchList->getDomains()
|
||||
->filter(fn ($domain) => $domain->getUpdatedAt()
|
||||
->diff(
|
||||
new \DateTimeImmutable('now'))->days >= 7
|
||||
|| $this->RDAPService::isToBeWatchClosely($domain, $domain->getUpdatedAt())
|
||||
) as $domain
|
||||
) {
|
||||
$updatedAt = $domain->getUpdatedAt();
|
||||
|
||||
try {
|
||||
$this->RDAPService->registerDomain($domain->getLdhName());
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->error('An update error email is sent to user {username}.', [
|
||||
'username' => $watchList->getUser()->getUserIdentifier(),
|
||||
'error' => $e,
|
||||
]);
|
||||
$this->sendEmailDomainUpdateError($domain, $watchList->getUser());
|
||||
}
|
||||
|
||||
$this->bus->dispatch(new ProcessDomainTrigger($watchList->getToken(), $domain->getLdhName(), $updatedAt));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws TransportExceptionInterface
|
||||
*/
|
||||
private function sendEmailDomainUpdateError(Domain $domain, User $user): void
|
||||
{
|
||||
$email = (new TemplatedEmail())
|
||||
->from(new Address($this->mailerSenderEmail, $this->mailerSenderName))
|
||||
->to($user->getEmail())
|
||||
->subject('An error occurred while updating a domain name')
|
||||
->htmlTemplate('emails/errors/domain_update.html.twig')
|
||||
->locale('en')
|
||||
->context([
|
||||
'domain' => $domain,
|
||||
]);
|
||||
|
||||
$this->mailer->send($email);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ namespace App\MessageHandler;
|
||||
|
||||
use App\Entity\WatchList;
|
||||
use App\Message\ProcessWatchListsTrigger;
|
||||
use App\Message\ProcessWatchListTrigger;
|
||||
use App\Message\UpdateDomainsFromWatchlist;
|
||||
use App\Repository\WatchListRepository;
|
||||
use Random\Randomizer;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
@@ -30,7 +30,7 @@ final readonly class ProcessWatchListsTriggerHandler
|
||||
|
||||
/** @var WatchList $watchList */
|
||||
foreach ($watchLists as $watchList) {
|
||||
$this->bus->dispatch(new ProcessWatchListTrigger($watchList->getToken()));
|
||||
$this->bus->dispatch(new UpdateDomainsFromWatchlist($watchList->getToken()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
80
src/MessageHandler/SendDomainEventNotifHandler.php
Normal file
80
src/MessageHandler/SendDomainEventNotifHandler.php
Normal file
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace App\MessageHandler;
|
||||
|
||||
use App\Config\TriggerAction;
|
||||
use App\Entity\Domain;
|
||||
use App\Entity\DomainEvent;
|
||||
use App\Entity\WatchList;
|
||||
use App\Entity\WatchListTrigger;
|
||||
use App\Message\SendDomainEventNotif;
|
||||
use App\Notifier\DomainUpdateNotification;
|
||||
use App\Repository\DomainRepository;
|
||||
use App\Repository\WatchListRepository;
|
||||
use App\Service\ChatNotificationService;
|
||||
use App\Service\StatService;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||
use Symfony\Component\Mailer\MailerInterface;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
use Symfony\Component\Messenger\Exception\ExceptionInterface;
|
||||
use Symfony\Component\Mime\Address;
|
||||
use Symfony\Component\Notifier\Recipient\Recipient;
|
||||
|
||||
#[AsMessageHandler]
|
||||
final readonly class SendDomainEventNotifHandler
|
||||
{
|
||||
private Address $sender;
|
||||
|
||||
public function __construct(
|
||||
string $mailerSenderEmail,
|
||||
string $mailerSenderName,
|
||||
private LoggerInterface $logger,
|
||||
private MailerInterface $mailer,
|
||||
private StatService $statService,
|
||||
private DomainRepository $domainRepository,
|
||||
private WatchListRepository $watchListRepository,
|
||||
private ChatNotificationService $chatNotificationService
|
||||
) {
|
||||
$this->sender = new Address($mailerSenderEmail, $mailerSenderName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws TransportExceptionInterface
|
||||
* @throws \Exception
|
||||
* @throws ExceptionInterface
|
||||
*/
|
||||
public function __invoke(SendDomainEventNotif $message): void
|
||||
{
|
||||
/** @var WatchList $watchList */
|
||||
$watchList = $this->watchListRepository->findOneBy(['token' => $message->watchListToken]);
|
||||
/** @var Domain $domain */
|
||||
$domain = $this->domainRepository->findOneBy(['ldhName' => $message->ldhName]);
|
||||
|
||||
/** @var DomainEvent $event */
|
||||
foreach ($domain->getEvents()->filter(fn ($event) => $message->updatedAt < $event->getDate() && $event->getDate() < new \DateTime()) as $event) {
|
||||
$watchListTriggers = $watchList->getWatchListTriggers()
|
||||
->filter(fn ($trigger) => $trigger->getEvent() === $event->getAction());
|
||||
|
||||
/** @var WatchListTrigger $watchListTrigger */
|
||||
foreach ($watchListTriggers->getIterator() as $watchListTrigger) {
|
||||
$this->logger->info('Action {event} has been detected on the domain name {ldhName}. A notification is sent to user {username}.', [
|
||||
'event' => $event->getAction(),
|
||||
'ldhName' => $message->ldhName,
|
||||
'username' => $watchList->getUser()->getUserIdentifier(),
|
||||
]);
|
||||
|
||||
$recipient = new Recipient($watchList->getUser()->getEmail());
|
||||
$notification = new DomainUpdateNotification($this->sender, $event);
|
||||
|
||||
if (TriggerAction::SendEmail == $watchListTrigger->getAction()) {
|
||||
$this->mailer->send($notification->asEmailMessage($recipient)->getMessage());
|
||||
} elseif (TriggerAction::SendChat == $watchListTrigger->getAction()) {
|
||||
$this->chatNotificationService->sendChatNotification($watchList, $notification);
|
||||
}
|
||||
|
||||
$this->statService->incrementStat('stats.alert.sent');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
100
src/MessageHandler/UpdateDomainsFromWatchlistHandler.php
Normal file
100
src/MessageHandler/UpdateDomainsFromWatchlistHandler.php
Normal file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace App\MessageHandler;
|
||||
|
||||
use App\Entity\Domain;
|
||||
use App\Entity\WatchList;
|
||||
use App\Message\OrderDomain;
|
||||
use App\Message\SendDomainEventNotif;
|
||||
use App\Message\UpdateDomainsFromWatchlist;
|
||||
use App\Notifier\DomainUpdateErrorNotification;
|
||||
use App\Repository\WatchListRepository;
|
||||
use App\Service\RDAPService;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||
use Symfony\Component\Mailer\MailerInterface;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
use Symfony\Component\Messenger\Exception\ExceptionInterface;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Component\Mime\Address;
|
||||
use Symfony\Component\Notifier\Recipient\Recipient;
|
||||
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
|
||||
|
||||
#[AsMessageHandler]
|
||||
final readonly class UpdateDomainsFromWatchlistHandler
|
||||
{
|
||||
private Address $sender;
|
||||
|
||||
public function __construct(
|
||||
private RDAPService $RDAPService,
|
||||
private MailerInterface $mailer,
|
||||
string $mailerSenderEmail,
|
||||
string $mailerSenderName,
|
||||
private MessageBusInterface $bus,
|
||||
private WatchListRepository $watchListRepository,
|
||||
private LoggerInterface $logger
|
||||
) {
|
||||
$this->sender = new Address($mailerSenderEmail, $mailerSenderName);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ExceptionInterface
|
||||
* @throws TransportExceptionInterface
|
||||
* @throws ClientExceptionInterface
|
||||
* @throws DecodingExceptionInterface
|
||||
* @throws RedirectionExceptionInterface
|
||||
* @throws ServerExceptionInterface
|
||||
* @throws \Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function __invoke(UpdateDomainsFromWatchlist $message): void
|
||||
{
|
||||
/** @var WatchList $watchList */
|
||||
$watchList = $this->watchListRepository->findOneBy(['token' => $message->watchListToken]);
|
||||
|
||||
$this->logger->info('Domain names from Watchlist {token} will be processed.', [
|
||||
'token' => $message->watchListToken,
|
||||
]);
|
||||
|
||||
/** @var Domain $domain */
|
||||
foreach ($watchList->getDomains()
|
||||
->filter(fn ($domain) => $domain->getUpdatedAt()
|
||||
->diff(new \DateTimeImmutable())->days >= 7
|
||||
|| (
|
||||
($domain->getUpdatedAt()
|
||||
->diff(new \DateTimeImmutable())->h * 60 + $domain->getUpdatedAt()
|
||||
->diff(new \DateTimeImmutable())->i) >= 50
|
||||
&& $this->RDAPService::isToBeWatchClosely($domain)
|
||||
)
|
||||
|| (count(array_intersect($domain->getStatus(), ['auto renew period', 'client hold', 'server hold'])) > 0
|
||||
&& $domain->getUpdatedAt()->diff(new \DateTimeImmutable())->days >= 1
|
||||
)
|
||||
) as $domain
|
||||
) {
|
||||
$updatedAt = $domain->getUpdatedAt();
|
||||
|
||||
try {
|
||||
$this->RDAPService->registerDomain($domain->getLdhName());
|
||||
$this->bus->dispatch(new SendDomainEventNotif($watchList->getToken(), $domain->getLdhName(), $updatedAt));
|
||||
} catch (NotFoundHttpException) {
|
||||
if (null !== $watchList->getConnector()) {
|
||||
$this->bus->dispatch(new OrderDomain($watchList->getToken(), $domain->getLdhName(), $updatedAt));
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
$this->logger->error('An update error email is sent to user {username}.', [
|
||||
'username' => $watchList->getUser()->getUserIdentifier(),
|
||||
'error' => $e,
|
||||
]);
|
||||
$email = (new DomainUpdateErrorNotification($this->sender, $domain))
|
||||
->asEmailMessage(new Recipient($watchList->getUser()->getEmail()));
|
||||
$this->mailer->send($email->getMessage());
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ namespace App\MessageHandler;
|
||||
|
||||
use App\Message\UpdateRdapServers;
|
||||
use App\Service\RDAPService;
|
||||
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
|
||||
@@ -14,8 +15,10 @@ use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||
#[AsMessageHandler]
|
||||
final readonly class UpdateRdapServersHandler
|
||||
{
|
||||
public function __construct(private RDAPService $RDAPService)
|
||||
{
|
||||
public function __construct(
|
||||
private RDAPService $RDAPService,
|
||||
private ParameterBagInterface $bag
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -29,17 +32,32 @@ final readonly class UpdateRdapServersHandler
|
||||
{
|
||||
/** @var \Throwable[] $throws */
|
||||
$throws = [];
|
||||
|
||||
try {
|
||||
$this->RDAPService->updateTldListIANA();
|
||||
$this->RDAPService->updateGTldListICANN();
|
||||
} catch (\Throwable $throwable) {
|
||||
$throws[] = $throwable;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->RDAPService->updateRDAPServers();
|
||||
$this->RDAPService->updateRDAPServersFromIANA();
|
||||
} catch (\Throwable $throwable) {
|
||||
$throws[] = $throwable;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->RDAPService->updateRDAPServersFromIANA();
|
||||
} catch (\Throwable $throwable) {
|
||||
$throws[] = $throwable;
|
||||
}
|
||||
|
||||
try {
|
||||
$this->RDAPService->updateRDAPServersFromFile($this->bag->get('custom_rdap_servers_file'));
|
||||
} catch (\Throwable $throwable) {
|
||||
$throws[] = $throwable;
|
||||
}
|
||||
|
||||
if (!empty($throws)) {
|
||||
throw $throws[0];
|
||||
}
|
||||
|
||||
59
src/Notifier/DomainOrderErrorNotification.php
Normal file
59
src/Notifier/DomainOrderErrorNotification.php
Normal file
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifier;
|
||||
|
||||
use App\Config\WebhookScheme;
|
||||
use App\Entity\Domain;
|
||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||
use Symfony\Component\Mime\Address;
|
||||
use Symfony\Component\Notifier\Message\ChatMessage;
|
||||
use Symfony\Component\Notifier\Message\EmailMessage;
|
||||
use Symfony\Component\Notifier\Message\PushMessage;
|
||||
use Symfony\Component\Notifier\Notification\Notification;
|
||||
use Symfony\Component\Notifier\Recipient\EmailRecipientInterface;
|
||||
use Symfony\Component\Notifier\Recipient\RecipientInterface;
|
||||
|
||||
class DomainOrderErrorNotification extends DomainWatchdogNotification
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Address $sender,
|
||||
private readonly Domain $domain
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function asChatMessage(?RecipientInterface $recipient = null, ?string $transport = null): ?ChatMessage
|
||||
{
|
||||
$webhookScheme = WebhookScheme::from($transport);
|
||||
|
||||
$ldhName = $this->domain->getLdhName();
|
||||
$this->subject("Error: Domain Order $ldhName")
|
||||
->content("Domain name $ldhName tried to be purchased. The attempt failed.")
|
||||
->importance(Notification::IMPORTANCE_HIGH);
|
||||
|
||||
return ChatMessage::fromNotification($this);
|
||||
}
|
||||
|
||||
public function asPushMessage(?RecipientInterface $recipient = null, ?string $transport = null): ?PushMessage
|
||||
{
|
||||
$ldhName = $this->domain->getLdhName();
|
||||
$this->subject("Error: Domain Order $ldhName")
|
||||
->content("Domain name $ldhName tried to be purchased. The attempt failed.")
|
||||
->importance(Notification::IMPORTANCE_HIGH);
|
||||
|
||||
return PushMessage::fromNotification($this);
|
||||
}
|
||||
|
||||
public function asEmailMessage(EmailRecipientInterface $recipient): EmailMessage
|
||||
{
|
||||
return new EmailMessage((new TemplatedEmail())
|
||||
->from($this->sender)
|
||||
->to($recipient->getEmail())
|
||||
->subject('An error occurred while ordering a domain name')
|
||||
->htmlTemplate('emails/errors/domain_order.html.twig')
|
||||
->locale('en')
|
||||
->context([
|
||||
'domain' => $this->domain,
|
||||
]));
|
||||
}
|
||||
}
|
||||
63
src/Notifier/DomainOrderNotification.php
Normal file
63
src/Notifier/DomainOrderNotification.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifier;
|
||||
|
||||
use App\Entity\Connector;
|
||||
use App\Entity\Domain;
|
||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||
use Symfony\Component\Mime\Address;
|
||||
use Symfony\Component\Mime\Email;
|
||||
use Symfony\Component\Notifier\Message\ChatMessage;
|
||||
use Symfony\Component\Notifier\Message\EmailMessage;
|
||||
use Symfony\Component\Notifier\Message\PushMessage;
|
||||
use Symfony\Component\Notifier\Notification\Notification;
|
||||
use Symfony\Component\Notifier\Recipient\EmailRecipientInterface;
|
||||
use Symfony\Component\Notifier\Recipient\RecipientInterface;
|
||||
|
||||
class DomainOrderNotification extends DomainWatchdogNotification
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Address $sender,
|
||||
private readonly Domain $domain,
|
||||
private readonly Connector $connector
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function asChatMessage(?RecipientInterface $recipient = null, ?string $transport = null): ?ChatMessage
|
||||
{
|
||||
$ldhName = $this->domain->getLdhName();
|
||||
$this
|
||||
->subject("Success: Domain Ordered $ldhName!")
|
||||
->content("Domain name $ldhName has just been purchased. The API provider did not return an error.")
|
||||
->importance(Notification::IMPORTANCE_HIGH);
|
||||
|
||||
return ChatMessage::fromNotification($this);
|
||||
}
|
||||
|
||||
public function asPushMessage(?RecipientInterface $recipient = null, ?string $transport = null): ?PushMessage
|
||||
{
|
||||
$ldhName = $this->domain->getLdhName();
|
||||
$this
|
||||
->subject("Success: Domain Ordered $ldhName!")
|
||||
->content("Domain name $ldhName has just been purchased. The API provider did not return an error.")
|
||||
->importance(Notification::IMPORTANCE_HIGH);
|
||||
|
||||
return PushMessage::fromNotification($this);
|
||||
}
|
||||
|
||||
public function asEmailMessage(EmailRecipientInterface $recipient): EmailMessage
|
||||
{
|
||||
return new EmailMessage((new TemplatedEmail())
|
||||
->from($this->sender)
|
||||
->to($recipient->getEmail())
|
||||
->priority(Email::PRIORITY_HIGHEST)
|
||||
->subject('A domain name has been ordered')
|
||||
->htmlTemplate('emails/success/domain_ordered.html.twig')
|
||||
->locale('en')
|
||||
->context([
|
||||
'domain' => $this->domain,
|
||||
'provider' => $this->connector->getProvider()->value,
|
||||
]));
|
||||
}
|
||||
}
|
||||
56
src/Notifier/DomainUpdateErrorNotification.php
Normal file
56
src/Notifier/DomainUpdateErrorNotification.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifier;
|
||||
|
||||
use App\Entity\Domain;
|
||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||
use Symfony\Component\Mime\Address;
|
||||
use Symfony\Component\Notifier\Message\ChatMessage;
|
||||
use Symfony\Component\Notifier\Message\EmailMessage;
|
||||
use Symfony\Component\Notifier\Message\PushMessage;
|
||||
use Symfony\Component\Notifier\Notification\Notification;
|
||||
use Symfony\Component\Notifier\Recipient\EmailRecipientInterface;
|
||||
use Symfony\Component\Notifier\Recipient\RecipientInterface;
|
||||
|
||||
class DomainUpdateErrorNotification extends DomainWatchdogNotification
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Address $sender,
|
||||
private readonly Domain $domain
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function asChatMessage(?RecipientInterface $recipient = null, ?string $transport = null): ?ChatMessage
|
||||
{
|
||||
$ldhName = $this->domain->getLdhName();
|
||||
$this->subject("Error: Domain Update $ldhName")
|
||||
->content("Domain name $ldhName tried to be updated. The attempt failed.")
|
||||
->importance(Notification::IMPORTANCE_MEDIUM);
|
||||
|
||||
return ChatMessage::fromNotification($this);
|
||||
}
|
||||
|
||||
public function asPushMessage(?RecipientInterface $recipient = null, ?string $transport = null): ?PushMessage
|
||||
{
|
||||
$ldhName = $this->domain->getLdhName();
|
||||
$this->subject("Error: Domain Update $ldhName")
|
||||
->content("Domain name $ldhName tried to be updated. The attempt failed.")
|
||||
->importance(Notification::IMPORTANCE_MEDIUM);
|
||||
|
||||
return PushMessage::fromNotification($this);
|
||||
}
|
||||
|
||||
public function asEmailMessage(EmailRecipientInterface $recipient): EmailMessage
|
||||
{
|
||||
return new EmailMessage((new TemplatedEmail())
|
||||
->from($this->sender)
|
||||
->to($recipient->getEmail())
|
||||
->subject('An error occurred while updating a domain name')
|
||||
->htmlTemplate('emails/errors/domain_update.html.twig')
|
||||
->locale('en')
|
||||
->context([
|
||||
'domain' => $this->domain,
|
||||
]));
|
||||
}
|
||||
}
|
||||
60
src/Notifier/DomainUpdateNotification.php
Normal file
60
src/Notifier/DomainUpdateNotification.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifier;
|
||||
|
||||
use App\Entity\DomainEvent;
|
||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||
use Symfony\Component\Mime\Address;
|
||||
use Symfony\Component\Mime\Email;
|
||||
use Symfony\Component\Notifier\Message\ChatMessage;
|
||||
use Symfony\Component\Notifier\Message\EmailMessage;
|
||||
use Symfony\Component\Notifier\Message\PushMessage;
|
||||
use Symfony\Component\Notifier\Notification\Notification;
|
||||
use Symfony\Component\Notifier\Recipient\EmailRecipientInterface;
|
||||
use Symfony\Component\Notifier\Recipient\RecipientInterface;
|
||||
|
||||
class DomainUpdateNotification extends DomainWatchdogNotification
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Address $sender,
|
||||
private readonly DomainEvent $domainEvent
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function asChatMessage(?RecipientInterface $recipient = null, ?string $transport = null): ?ChatMessage
|
||||
{
|
||||
$ldhName = $this->domainEvent->getDomain()->getLdhName();
|
||||
$action = $this->domainEvent->getAction();
|
||||
$this->subject("Domain changed $ldhName ($action)")
|
||||
->content("Domain name $ldhName information has been updated ($action).")
|
||||
->importance(Notification::IMPORTANCE_HIGH);
|
||||
|
||||
return ChatMessage::fromNotification($this);
|
||||
}
|
||||
|
||||
public function asPushMessage(?RecipientInterface $recipient = null, ?string $transport = null): ?PushMessage
|
||||
{
|
||||
$ldhName = $this->domainEvent->getDomain()->getLdhName();
|
||||
$action = $this->domainEvent->getAction();
|
||||
$this->subject("Domain changed $ldhName ($action)")
|
||||
->content("Domain name $ldhName information has been updated ($action).")
|
||||
->importance(Notification::IMPORTANCE_HIGH);
|
||||
|
||||
return PushMessage::fromNotification($this);
|
||||
}
|
||||
|
||||
public function asEmailMessage(EmailRecipientInterface $recipient): EmailMessage
|
||||
{
|
||||
return new EmailMessage((new TemplatedEmail())
|
||||
->from($this->sender)
|
||||
->to($recipient->getEmail())
|
||||
->priority(Email::PRIORITY_HIGHEST)
|
||||
->subject('A domain name has been changed')
|
||||
->htmlTemplate('emails/success/domain_updated.html.twig')
|
||||
->locale('en')
|
||||
->context([
|
||||
'event' => $this->domainEvent,
|
||||
]));
|
||||
}
|
||||
}
|
||||
11
src/Notifier/DomainWatchdogNotification.php
Normal file
11
src/Notifier/DomainWatchdogNotification.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifier;
|
||||
|
||||
use Symfony\Component\Notifier\Notification\ChatNotificationInterface;
|
||||
use Symfony\Component\Notifier\Notification\Notification;
|
||||
use Symfony\Component\Notifier\Notification\PushNotificationInterface;
|
||||
|
||||
abstract class DomainWatchdogNotification extends Notification implements ChatNotificationInterface, PushNotificationInterface
|
||||
{
|
||||
}
|
||||
31
src/Notifier/TestChatNotification.php
Normal file
31
src/Notifier/TestChatNotification.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Notifier;
|
||||
|
||||
use Symfony\Component\Notifier\Message\ChatMessage;
|
||||
use Symfony\Component\Notifier\Message\PushMessage;
|
||||
use Symfony\Component\Notifier\Notification\Notification;
|
||||
use Symfony\Component\Notifier\Recipient\RecipientInterface;
|
||||
|
||||
class TestChatNotification extends DomainWatchdogNotification
|
||||
{
|
||||
public function asChatMessage(?RecipientInterface $recipient = null, ?string $transport = null): ?ChatMessage
|
||||
{
|
||||
$this
|
||||
->subject('Test notification')
|
||||
->content('This is a test message. If you can read me, this Webhook is configured correctly')
|
||||
->importance(Notification::IMPORTANCE_LOW);
|
||||
|
||||
return ChatMessage::fromNotification($this);
|
||||
}
|
||||
|
||||
public function asPushMessage(?RecipientInterface $recipient = null, ?string $transport = null): ?PushMessage
|
||||
{
|
||||
$this
|
||||
->subject('Test notification')
|
||||
->content('This is a test message. If you can read me, this Webhook is configured correctly')
|
||||
->importance(Notification::IMPORTANCE_LOW);
|
||||
|
||||
return PushMessage::fromNotification($this);
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ final readonly class SendNotifWatchListTriggerSchedule implements ScheduleProvid
|
||||
{
|
||||
return (new Schedule())
|
||||
->add(
|
||||
RecurringMessage::every('1 day', new ProcessWatchListsTrigger()),
|
||||
RecurringMessage::every('1 hour', new ProcessWatchListsTrigger()),
|
||||
)
|
||||
->stateful($this->cache);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ readonly class EmailVerifier
|
||||
/**
|
||||
* @throws TransportExceptionInterface
|
||||
*/
|
||||
public function sendEmailConfirmation(string $verifyEmailRouteName, User $user, TemplatedEmail $email): void
|
||||
public function sendEmailConfirmation(string $verifyEmailRouteName, User $user, TemplatedEmail $email): TemplatedEmail
|
||||
{
|
||||
$signatureComponents = $this->verifyEmailHelper->generateSignature(
|
||||
$verifyEmailRouteName,
|
||||
@@ -39,6 +39,8 @@ readonly class EmailVerifier
|
||||
$email->context($context);
|
||||
|
||||
$this->mailer->send($email);
|
||||
|
||||
return $email;
|
||||
}
|
||||
|
||||
public function handleEmailConfirmation(Request $request, User $user): void
|
||||
|
||||
@@ -11,6 +11,7 @@ use Symfony\Component\HttpFoundation\Cookie;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\KernelInterface;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;
|
||||
@@ -21,6 +22,7 @@ class JWTAuthenticator implements AuthenticationSuccessHandlerInterface
|
||||
public function __construct(
|
||||
protected JWTTokenManagerInterface $jwtManager,
|
||||
protected EventDispatcherInterface $dispatcher,
|
||||
protected KernelInterface $kernel
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -43,10 +45,10 @@ class JWTAuthenticator implements AuthenticationSuccessHandlerInterface
|
||||
new Cookie(
|
||||
'BEARER',
|
||||
$jwt,
|
||||
time() + 7200, // expiration
|
||||
time() + 604800, // expiration
|
||||
'/',
|
||||
null,
|
||||
false,
|
||||
!$this->kernel->isDebug(),
|
||||
true,
|
||||
false,
|
||||
'strict'
|
||||
|
||||
@@ -75,7 +75,7 @@ class OAuthAuthenticator extends OAuth2Authenticator implements AuthenticationEn
|
||||
new Cookie(
|
||||
'BEARER',
|
||||
$token,
|
||||
time() + 7200, // expiration
|
||||
time() + 604800, // expiration
|
||||
'/',
|
||||
null,
|
||||
true,
|
||||
|
||||
74
src/Service/ChatNotificationService.php
Normal file
74
src/Service/ChatNotificationService.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Config\WebhookScheme;
|
||||
use App\Entity\WatchList;
|
||||
use App\Notifier\DomainWatchdogNotification;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\Notifier\Exception\InvalidArgumentException;
|
||||
use Symfony\Component\Notifier\Recipient\NoRecipient;
|
||||
use Symfony\Component\Notifier\Transport\AbstractTransportFactory;
|
||||
use Symfony\Component\Notifier\Transport\Dsn;
|
||||
|
||||
readonly class ChatNotificationService
|
||||
{
|
||||
public function __construct(
|
||||
private LoggerInterface $logger
|
||||
) {
|
||||
}
|
||||
|
||||
public function sendChatNotification(WatchList $watchList, DomainWatchdogNotification $notification): void
|
||||
{
|
||||
$webhookDsn = $watchList->getWebhookDsn();
|
||||
if (null !== $webhookDsn && 0 !== count($webhookDsn)) {
|
||||
foreach ($webhookDsn as $dsnString) {
|
||||
try {
|
||||
$dsn = new Dsn($dsnString);
|
||||
} catch (InvalidArgumentException $exception) {
|
||||
throw new BadRequestHttpException($exception->getMessage());
|
||||
}
|
||||
|
||||
$scheme = $dsn->getScheme();
|
||||
$webhookScheme = WebhookScheme::tryFrom($scheme);
|
||||
|
||||
if (null === $webhookScheme) {
|
||||
throw new BadRequestHttpException("The DSN scheme ($scheme) is not supported");
|
||||
}
|
||||
|
||||
$transportFactoryClass = $webhookScheme->getChatTransportFactory();
|
||||
/** @var AbstractTransportFactory $transportFactory */
|
||||
$transportFactory = new $transportFactoryClass();
|
||||
|
||||
$push = $notification->asPushMessage(new NoRecipient());
|
||||
$chat = $notification->asChatMessage(new NoRecipient(), $webhookScheme->value);
|
||||
|
||||
try {
|
||||
$factory = $transportFactory->create($dsn);
|
||||
|
||||
if ($factory->supports($push)) {
|
||||
$factory->send($push);
|
||||
} elseif ($factory->supports($chat)) {
|
||||
$factory->send($chat);
|
||||
} else {
|
||||
throw new BadRequestHttpException('Unsupported message type');
|
||||
}
|
||||
|
||||
$this->logger->info('Chat message sent with {schema} for Watchlist {token}',
|
||||
[
|
||||
'scheme' => $webhookScheme->name,
|
||||
'token' => $watchList->getToken(),
|
||||
]);
|
||||
} catch (\Throwable $exception) {
|
||||
$this->logger->error('Unable to send a chat message to {scheme} for Watchlist {token}',
|
||||
[
|
||||
'scheme' => $webhookScheme->name,
|
||||
'token' => $watchList->getToken(),
|
||||
]);
|
||||
throw new BadRequestHttpException($exception->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\Connector;
|
||||
|
||||
abstract class AbstractConnector implements ConnectorInterface
|
||||
{
|
||||
protected array $authData;
|
||||
|
||||
public function authenticate(array $authData)
|
||||
{
|
||||
$this->authData = $authData;
|
||||
}
|
||||
}
|
||||
64
src/Service/Connector/AbstractProvider.php
Normal file
64
src/Service/Connector/AbstractProvider.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\Connector;
|
||||
|
||||
use App\Entity\Domain;
|
||||
use Psr\Cache\CacheItemInterface;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
abstract class AbstractProvider
|
||||
{
|
||||
protected array $authData;
|
||||
|
||||
public function __construct(
|
||||
protected CacheItemPoolInterface $cacheItemPool
|
||||
) {
|
||||
}
|
||||
|
||||
abstract public static function verifyAuthData(array $authData, HttpClientInterface $client): array;
|
||||
|
||||
abstract public function orderDomain(Domain $domain, bool $dryRun): void;
|
||||
|
||||
public function isSupported(Domain ...$domainList): bool
|
||||
{
|
||||
$item = $this->getCachedTldList();
|
||||
if (!$item->isHit()) {
|
||||
$supportedTldList = $this->getSupportedTldList();
|
||||
$item
|
||||
->set($supportedTldList)
|
||||
->expiresAfter(new \DateInterval('PT1H'));
|
||||
$this->cacheItemPool->saveDeferred($item);
|
||||
} else {
|
||||
$supportedTldList = $item->get();
|
||||
}
|
||||
|
||||
$extensionList = [];
|
||||
foreach ($domainList as $domain) {
|
||||
// We want to check the support of TLDs and SLDs here.
|
||||
// For example, it is not enough for the Connector to support .fr for it to support the domain name example.asso.fr.
|
||||
// It must support .asso.fr.
|
||||
$extension = explode('.', $domain->getLdhName(), 2)[1];
|
||||
if (!in_array($extension, $extensionList)) {
|
||||
$extensionList[] = $extension;
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($extensionList as $extension) {
|
||||
if (!in_array($extension, $supportedTldList)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function authenticate(array $authData): void
|
||||
{
|
||||
$this->authData = $authData;
|
||||
}
|
||||
|
||||
abstract protected function getCachedTldList(): CacheItemInterface;
|
||||
|
||||
abstract protected function getSupportedTldList(): array;
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service\Connector;
|
||||
|
||||
use App\Entity\Domain;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
interface ConnectorInterface
|
||||
{
|
||||
public function authenticate(array $authData);
|
||||
|
||||
public function orderDomain(Domain $domain, bool $dryRun): void;
|
||||
|
||||
public static function verifyAuthData(array $authData, HttpClientInterface $client): array;
|
||||
}
|
||||
@@ -3,18 +3,21 @@
|
||||
namespace App\Service\Connector;
|
||||
|
||||
use App\Entity\Domain;
|
||||
use http\Exception\InvalidArgumentException;
|
||||
use Psr\Cache\CacheItemInterface;
|
||||
use Symfony\Component\HttpClient\HttpOptions;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\RedirectionExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\ServerExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
class GandiConnector extends AbstractConnector
|
||||
class GandiProvider extends AbstractProvider
|
||||
{
|
||||
private const BASE_URL = 'https://api.gandi.net/v5';
|
||||
private const BASE_URL = 'https://api.gandi.net';
|
||||
|
||||
public function __construct(private HttpClientInterface $client)
|
||||
{
|
||||
@@ -30,12 +33,12 @@ class GandiConnector extends AbstractConnector
|
||||
public function orderDomain(Domain $domain, bool $dryRun = false): void
|
||||
{
|
||||
if (!$domain->getDeleted()) {
|
||||
throw new InvalidArgumentException('The domain name still appears in the WHOIS database');
|
||||
throw new \InvalidArgumentException('The domain name still appears in the WHOIS database');
|
||||
}
|
||||
|
||||
$ldhName = $domain->getLdhName();
|
||||
if (!$ldhName) {
|
||||
throw new InvalidArgumentException('Domain name cannot be null');
|
||||
throw new \InvalidArgumentException('Domain name cannot be null');
|
||||
}
|
||||
|
||||
$authData = self::verifyAuthData($this->authData, $this->client);
|
||||
@@ -130,4 +133,32 @@ class GandiConnector extends AbstractConnector
|
||||
|
||||
return $authDataReturned;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws TransportExceptionInterface
|
||||
* @throws ServerExceptionInterface
|
||||
* @throws RedirectionExceptionInterface
|
||||
* @throws DecodingExceptionInterface
|
||||
* @throws ClientExceptionInterface
|
||||
*/
|
||||
protected function getSupportedTldList(): array
|
||||
{
|
||||
$authData = self::verifyAuthData($this->authData, $this->client);
|
||||
|
||||
$response = $this->client->request('GET', '/v5/domain/tlds', (new HttpOptions())
|
||||
->setAuthBearer($authData['token'])
|
||||
->setHeader('Accept', 'application/json')
|
||||
->setBaseUri(self::BASE_URL)
|
||||
->toArray())->toArray();
|
||||
|
||||
return array_map(fn ($tld) => $tld['name'], $response);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws \Psr\Cache\InvalidArgumentException
|
||||
*/
|
||||
protected function getCachedTldList(): CacheItemInterface
|
||||
{
|
||||
return $this->cacheItemPool->getItem('app.provider.ovh.supported-tld');
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,12 @@
|
||||
namespace App\Service\Connector;
|
||||
|
||||
use App\Entity\Domain;
|
||||
use Psr\Cache\CacheItemInterface;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autoconfigure;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
#[Autoconfigure(public: true)]
|
||||
class NamecheapConnector extends AbstractConnector
|
||||
class NamecheapConnector extends AbstractProvider
|
||||
{
|
||||
public const BASE_URL = 'https://api.namecheap.com/xml.response';
|
||||
|
||||
@@ -83,4 +84,14 @@ class NamecheapConnector extends AbstractConnector
|
||||
{
|
||||
return $authData;
|
||||
}
|
||||
|
||||
protected function getCachedTldList(): CacheItemInterface
|
||||
{
|
||||
// TODO: Implement getCachedTldList() method.
|
||||
}
|
||||
|
||||
protected function getSupportedTldList(): array
|
||||
{
|
||||
// TODO: Implement getSupportedTldList() method.
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,22 @@
|
||||
namespace App\Service\Connector;
|
||||
|
||||
use App\Entity\Domain;
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
use Ovh\Api;
|
||||
use Ovh\Exceptions\InvalidParameterException;
|
||||
use Psr\Cache\CacheItemInterface;
|
||||
use Psr\Cache\InvalidArgumentException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||
|
||||
class OvhConnector extends AbstractConnector
|
||||
class OvhProvider extends AbstractProvider
|
||||
{
|
||||
public const REQUIRED_ROUTES = [
|
||||
[
|
||||
'method' => 'GET',
|
||||
'path' => '/domain/extensions',
|
||||
],
|
||||
[
|
||||
'method' => 'GET',
|
||||
'path' => '/order/cart',
|
||||
@@ -45,12 +53,12 @@ class OvhConnector extends AbstractConnector
|
||||
public function orderDomain(Domain $domain, bool $dryRun = false): void
|
||||
{
|
||||
if (!$domain->getDeleted()) {
|
||||
throw new \Exception('The domain name still appears in the WHOIS database');
|
||||
throw new \InvalidArgumentException('The domain name still appears in the WHOIS database');
|
||||
}
|
||||
|
||||
$ldhName = $domain->getLdhName();
|
||||
if (!$ldhName) {
|
||||
throw new \Exception('Domain name cannot be null');
|
||||
throw new \InvalidArgumentException('Domain name cannot be null');
|
||||
}
|
||||
|
||||
$authData = self::verifyAuthData($this->authData, $this->client);
|
||||
@@ -87,7 +95,7 @@ class OvhConnector extends AbstractConnector
|
||||
);
|
||||
if (empty($offer)) {
|
||||
$conn->delete("/order/cart/{$cartId}");
|
||||
throw new \Exception('Cannot buy this domain name');
|
||||
throw new \InvalidArgumentException('Cannot buy this domain name');
|
||||
}
|
||||
|
||||
$item = $conn->post("/order/cart/{$cartId}/domain", [
|
||||
@@ -163,14 +171,18 @@ class OvhConnector extends AbstractConnector
|
||||
$consumerKey
|
||||
);
|
||||
|
||||
$res = $conn->get('/auth/currentCredential');
|
||||
if (null !== $res['expiration'] && new \DateTime($res['expiration']) < new \DateTime()) {
|
||||
throw new \Exception('These credentials have expired');
|
||||
}
|
||||
try {
|
||||
$res = $conn->get('/auth/currentCredential');
|
||||
if (null !== $res['expiration'] && new \DateTime($res['expiration']) < new \DateTime()) {
|
||||
throw new BadRequestHttpException('These credentials have expired');
|
||||
}
|
||||
|
||||
$status = $res['status'];
|
||||
if ('validated' !== $status) {
|
||||
throw new \Exception("The status of these credentials is not valid ($status)");
|
||||
$status = $res['status'];
|
||||
if ('validated' !== $status) {
|
||||
throw new BadRequestHttpException("The status of these credentials is not valid ($status)");
|
||||
}
|
||||
} catch (ClientException $exception) {
|
||||
throw new BadRequestHttpException($exception->getMessage());
|
||||
}
|
||||
|
||||
foreach (self::REQUIRED_ROUTES as $requiredRoute) {
|
||||
@@ -186,7 +198,7 @@ class OvhConnector extends AbstractConnector
|
||||
}
|
||||
|
||||
if (!$ok) {
|
||||
throw new BadRequestHttpException('The credentials provided do not have enough permissions to purchase a domain name.');
|
||||
throw new BadRequestHttpException('This Connector does not have enough permissions on the Provider API. Please recreate this Connector.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -202,4 +214,33 @@ class OvhConnector extends AbstractConnector
|
||||
'waiveRetractationPeriod' => $waiveRetractationPeriod,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidParameterException
|
||||
* @throws \JsonException
|
||||
* @throws \Exception
|
||||
*/
|
||||
protected function getSupportedTldList(): array
|
||||
{
|
||||
$authData = self::verifyAuthData($this->authData, $this->client);
|
||||
|
||||
$conn = new Api(
|
||||
$authData['appKey'],
|
||||
$authData['appSecret'],
|
||||
$authData['apiEndpoint'],
|
||||
$authData['consumerKey']
|
||||
);
|
||||
|
||||
return $conn->get('/domain/extensions', [
|
||||
'ovhSubsidiary' => $authData['ovhSubsidiary'],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvalidArgumentException
|
||||
*/
|
||||
protected function getCachedTldList(): CacheItemInterface
|
||||
{
|
||||
return $this->cacheItemPool->getItem('app.provider.ovh.supported-tld');
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpClient\Exception\ClientException;
|
||||
use Symfony\Component\HttpFoundation\Exception\BadRequestException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\Yaml\Yaml;
|
||||
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface;
|
||||
use Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface;
|
||||
@@ -73,7 +74,17 @@ readonly class RDAPService
|
||||
'xn--hlcj6aya9esc7a',
|
||||
];
|
||||
|
||||
public const IMPORTANT_EVENTS = [EventAction::Deletion->value, EventAction::Expiration->value];
|
||||
private const IMPORTANT_EVENTS = [EventAction::Deletion->value, EventAction::Expiration->value];
|
||||
private const IMPORTANT_STATUS = [
|
||||
'redemption period',
|
||||
'pending delete',
|
||||
'pending create',
|
||||
'pending renew',
|
||||
'pending restore',
|
||||
'pending transfer',
|
||||
'pending update',
|
||||
'add period',
|
||||
];
|
||||
|
||||
public function __construct(private HttpClientInterface $client,
|
||||
private EntityRepository $entityRepository,
|
||||
@@ -86,7 +97,8 @@ readonly class RDAPService
|
||||
private RdapServerRepository $rdapServerRepository,
|
||||
private TldRepository $tldRepository,
|
||||
private EntityManagerInterface $em,
|
||||
private LoggerInterface $logger
|
||||
private LoggerInterface $logger,
|
||||
private StatService $statService
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -96,10 +108,11 @@ readonly class RDAPService
|
||||
*
|
||||
* @throws \Exception
|
||||
*/
|
||||
public static function isToBeWatchClosely(Domain $domain, \DateTimeImmutable $updatedAt): bool
|
||||
public static function isToBeWatchClosely(Domain $domain): bool
|
||||
{
|
||||
if ($updatedAt->diff(new \DateTimeImmutable('now'))->days < 1) {
|
||||
return false;
|
||||
$status = $domain->getStatus();
|
||||
if ((!empty($status) && count(array_intersect($status, self::IMPORTANT_STATUS))) || $domain->getDeleted()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/** @var DomainEvent[] $events */
|
||||
@@ -162,6 +175,8 @@ readonly class RDAPService
|
||||
]);
|
||||
|
||||
try {
|
||||
$this->statService->incrementStat('stats.rdap_queries.count');
|
||||
|
||||
$res = $this->client->request(
|
||||
'GET', $rdapServerUrl.'domain/'.$idnDomain
|
||||
)->toArray();
|
||||
@@ -213,6 +228,11 @@ readonly class RDAPService
|
||||
$this->em->persist($domain);
|
||||
$this->em->flush();
|
||||
|
||||
/** @var DomainEvent $event */
|
||||
foreach ($domain->getEvents()->getIterator() as $event) {
|
||||
$event->setDeleted(true);
|
||||
}
|
||||
|
||||
foreach ($res['events'] as $rdapEvent) {
|
||||
if ($rdapEvent['eventAction'] === EventAction::LastUpdateOfRDAPDatabase->value) {
|
||||
continue;
|
||||
@@ -229,7 +249,14 @@ readonly class RDAPService
|
||||
}
|
||||
$domain->addEvent($event
|
||||
->setAction($rdapEvent['eventAction'])
|
||||
->setDate(new \DateTimeImmutable($rdapEvent['eventDate'])));
|
||||
->setDate(new \DateTimeImmutable($rdapEvent['eventDate']))
|
||||
->setDeleted(false)
|
||||
);
|
||||
}
|
||||
|
||||
/** @var DomainEntity $domainEntity */
|
||||
foreach ($domain->getDomainEntities()->getIterator() as $domainEntity) {
|
||||
$domainEntity->setDeleted(true);
|
||||
}
|
||||
|
||||
if (array_key_exists('entities', $res) && is_array($res['entities'])) {
|
||||
@@ -270,8 +297,9 @@ readonly class RDAPService
|
||||
$domain->addDomainEntity($domainEntity
|
||||
->setDomain($domain)
|
||||
->setEntity($entity)
|
||||
->setRoles($roles))
|
||||
->updateTimestamps();
|
||||
->setRoles($roles)
|
||||
->setDeleted(false)
|
||||
);
|
||||
|
||||
$this->em->persist($domainEntity);
|
||||
$this->em->flush();
|
||||
@@ -279,10 +307,18 @@ readonly class RDAPService
|
||||
}
|
||||
|
||||
if (array_key_exists('nameservers', $res) && is_array($res['nameservers'])) {
|
||||
$domain->getNameservers()->clear();
|
||||
|
||||
foreach ($res['nameservers'] as $rdapNameserver) {
|
||||
$nameserver = $this->nameserverRepository->findOneBy([
|
||||
'ldhName' => strtolower($rdapNameserver['ldhName']),
|
||||
]);
|
||||
|
||||
$domainNS = $domain->getNameservers()->findFirst(fn (int $key, Nameserver $ns) => $ns->getLdhName() === $rdapNameserver['ldhName']);
|
||||
|
||||
if (null !== $domainNS) {
|
||||
$nameserver = $domainNS;
|
||||
}
|
||||
if (null === $nameserver) {
|
||||
$nameserver = new Nameserver();
|
||||
}
|
||||
@@ -349,7 +385,7 @@ readonly class RDAPService
|
||||
if (false === $lastDotPosition) {
|
||||
throw new BadRequestException('Domain must contain at least one dot');
|
||||
}
|
||||
$tld = strtolower(substr($domain, $lastDotPosition + 1));
|
||||
$tld = strtolower(idn_to_ascii(substr($domain, $lastDotPosition + 1)));
|
||||
|
||||
return $this->tldRepository->findOneBy(['tld' => $tld]);
|
||||
}
|
||||
@@ -365,7 +401,7 @@ readonly class RDAPService
|
||||
|
||||
if (null === $entity) {
|
||||
$entity = new Entity();
|
||||
} else {
|
||||
|
||||
$this->logger->info('The entity {handle} was not known to this Domain Watchdog instance.', [
|
||||
'handle' => $rdapEntity['handle'],
|
||||
]);
|
||||
@@ -392,6 +428,14 @@ readonly class RDAPService
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/** @var EntityEvent $event */
|
||||
foreach ($entity->getEvents()->getIterator() as $event) {
|
||||
$event->setDeleted(true);
|
||||
}
|
||||
|
||||
$this->em->persist($entity);
|
||||
$this->em->flush();
|
||||
|
||||
foreach ($rdapEntity['events'] as $rdapEntityEvent) {
|
||||
$eventAction = $rdapEntityEvent['eventAction'];
|
||||
if ($eventAction === EventAction::LastChanged->value || $eventAction === EventAction::LastUpdateOfRDAPDatabase->value) {
|
||||
@@ -403,13 +447,15 @@ readonly class RDAPService
|
||||
]);
|
||||
|
||||
if (null !== $event) {
|
||||
$event->setDeleted(false);
|
||||
continue;
|
||||
}
|
||||
$entity->addEvent(
|
||||
(new EntityEvent())
|
||||
->setEntity($entity)
|
||||
->setAction($rdapEntityEvent['eventAction'])
|
||||
->setDate(new \DateTimeImmutable($rdapEntityEvent['eventDate'])));
|
||||
->setDate(new \DateTimeImmutable($rdapEntityEvent['eventDate']))
|
||||
->setDeleted(false));
|
||||
}
|
||||
|
||||
return $entity;
|
||||
@@ -423,14 +469,23 @@ readonly class RDAPService
|
||||
* @throws ClientExceptionInterface
|
||||
* @throws ORMException
|
||||
*/
|
||||
public function updateRDAPServers(): void
|
||||
public function updateRDAPServersFromIANA(): void
|
||||
{
|
||||
$this->logger->info('Started updating the RDAP server list.');
|
||||
$this->logger->info('Start of update the RDAP server list from IANA.');
|
||||
|
||||
$dnsRoot = $this->client->request(
|
||||
'GET', 'https://data.iana.org/rdap/dns.json'
|
||||
)->toArray();
|
||||
|
||||
$this->updateRDAPServers($dnsRoot);
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ORMException
|
||||
* @throws \Exception
|
||||
*/
|
||||
private function updateRDAPServers(array $dnsRoot): void
|
||||
{
|
||||
foreach ($dnsRoot['services'] as $service) {
|
||||
foreach ($service[0] as $tld) {
|
||||
if ('' === $tld) {
|
||||
@@ -442,7 +497,10 @@ readonly class RDAPService
|
||||
if (null === $server) {
|
||||
$server = new RdapServer();
|
||||
}
|
||||
$server->setTld($tldReference)->setUrl($rdapServerUrl)->updateTimestamps();
|
||||
$server
|
||||
->setTld($tldReference)
|
||||
->setUrl($rdapServerUrl)
|
||||
->setUpdatedAt(new \DateTimeImmutable(array_key_exists('publication', $dnsRoot) ? $dnsRoot['publication'] : 'now'));
|
||||
|
||||
$this->em->persist($server);
|
||||
}
|
||||
@@ -451,6 +509,19 @@ readonly class RDAPService
|
||||
$this->em->flush();
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws ORMException
|
||||
*/
|
||||
public function updateRDAPServersFromFile(string $fileName): void
|
||||
{
|
||||
if (!file_exists($fileName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->logger->info('Start of update the RDAP server list from custom config file.');
|
||||
$this->updateRDAPServers(Yaml::parseFile($fileName));
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws TransportExceptionInterface
|
||||
* @throws ServerExceptionInterface
|
||||
|
||||
26
src/Service/StatService.php
Normal file
26
src/Service/StatService.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
|
||||
readonly class StatService
|
||||
{
|
||||
public function __construct(
|
||||
private CacheItemPoolInterface $pool
|
||||
) {
|
||||
}
|
||||
|
||||
public function incrementStat(string $key): bool
|
||||
{
|
||||
try {
|
||||
$item = $this->pool->getItem($key);
|
||||
$item->set(($item->get() ?? 0) + 1);
|
||||
|
||||
return $this->pool->save($item);
|
||||
} catch (\Throwable) {
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user