docs: add documentation on MessageHandler

This commit is contained in:
Maël Gangloff
2024-12-03 21:10:38 +01:00
parent 2a865a68da
commit dc3879c4c5
7 changed files with 151 additions and 75 deletions

View File

@@ -363,7 +363,7 @@ class WatchListController extends AbstractController
}
usort($domains, function (Domain $d1, Domain $d2) {
$IMPORTANT_STATUS = ['pending delete', 'redemption period', 'auto renew period'];
$IMPORTANT_STATUS = ['pending delete', 'redemption period'];
/** @var \DateTimeImmutable $exp1 */
$exp1 = $d1->getEvents()->findFirst(fn (int $key, DomainEvent $e) => !$e->getDeleted() && 'expiration' === $e->getAction())->getDate();

View File

@@ -56,44 +56,73 @@ final readonly class OrderDomainHandler
$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}.', [
/*
* We make sure that the domain name is marked absent from WHOIS in the database before continuing.
* A connector must also be associated with the Watchlist to allow the purchase of the domain name.
* Otherwise, we do nothing.
*/
if (null === $connector || !$domain->getDeleted()) {
return;
}
$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);
/*
* The user is authenticated to ensure that the credentials are still valid.
* If no errors occur, the purchase is attempted.
*/
$connectorProvider->authenticate($connector->getAuthData());
$connectorProvider->orderDomain($domain, $this->kernel->isDebug());
/*
* If the purchase was successful, the statistics are updated and a success message is sent to the user.
*/
$this->logger->notice('Watchlist {watchlist} is linked to connector {connector}. A purchase was successfully made for domain {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();
$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) {
/*
* The purchase was not successful (for several possible reasons that we have not determined).
* The user is informed and the exception is raised, which may allow you to try again.
*/
$this->logger->warning('Unable to complete purchase. An error message is sent to user {username}.', [
'username' => $watchList->getUser()->getUserIdentifier(),
]);
/** @var AbstractProvider $connectorProvider */
$connectorProvider = $this->locator->get($connectorProviderClass);
$connectorProvider->authenticate($connector->getAuthData());
$this->statService->incrementStat('stats.domain.purchase.failed');
$notification = (new DomainOrderErrorNotification($this->sender, $domain));
$this->mailer->send($notification->asEmailMessage(new Recipient($watchList->getUser()->getEmail()))->getMessage());
$this->chatNotificationService->sendChatNotification($watchList, $notification);
$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;
}
throw $exception;
}
}
}

View File

@@ -25,6 +25,11 @@ final readonly class ProcessWatchListsTriggerHandler
*/
public function __invoke(ProcessWatchListsTrigger $message): void
{
/*
* We shuffle the watch lists to process them in an order that we consider random.
* The shuffling is provided by a high-level API of a CSPRNG number generator.
*/
$randomizer = new Randomizer();
$watchLists = $randomizer->shuffleArray($this->watchListRepository->findAll());

View File

@@ -51,11 +51,19 @@ final readonly class SendDomainEventNotifHandler
/** @var Domain $domain */
$domain = $this->domainRepository->findOneBy(['ldhName' => $message->ldhName]);
/*
* For each new event whose date is after the domain name update date (before the current domain name update)
*/
/** @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());
/*
* For each trigger, we perform the appropriate action: send email or send push notification (for now)
*/
/** @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}.', [

View File

@@ -60,6 +60,13 @@ final readonly class UpdateDomainsFromWatchlistHandler
'token' => $message->watchListToken,
]);
/*
* A domain name is updated if one or more of these conditions are met:
* - was updated more than 7 days ago
* - has statuses that suggest it will expire soon AND was updated more than an hour ago
* - has specific statuses that suggest there is a dispute between the registrant and the registrar AND was updated more than a day ago
*/
/** @var Domain $domain */
foreach ($watchList->getDomains()
->filter(fn ($domain) => $domain->getUpdatedAt()
@@ -78,13 +85,25 @@ final readonly class UpdateDomainsFromWatchlistHandler
$updatedAt = $domain->getUpdatedAt();
try {
/*
* Domain name update
* We send messages that correspond to the sending of notifications that will not be processed here.
*/
$this->RDAPService->registerDomain($domain->getLdhName());
$this->bus->dispatch(new SendDomainEventNotif($watchList->getToken(), $domain->getLdhName(), $updatedAt));
} catch (NotFoundHttpException) {
if (null !== $watchList->getConnector()) {
/*
* If the domain name no longer appears in the WHOIS AND a connector is associated with this Watchlist,
* this connector is used to purchase the domain name.
*/
$this->bus->dispatch(new OrderDomain($watchList->getToken(), $domain->getLdhName(), $updatedAt));
}
} catch (\Throwable $e) {
/*
* In case of another unknown error,
* the owner of the Watchlist is informed that an error occurred in updating the domain name.
*/
$this->logger->error('An update error email is sent to user {username}.', [
'username' => $watchList->getUser()->getUserIdentifier(),
'error' => $e,

View File

@@ -33,6 +33,11 @@ final readonly class UpdateRdapServersHandler
/** @var \Throwable[] $throws */
$throws = [];
/*
* First, we update the list of TLDs from IANA because it is the official list of TLDs.
* Then, we update from ICANN because it allows to have information about generic top-level domains (gTLDs).
*/
try {
$this->RDAPService->updateTldListIANA();
$this->RDAPService->updateGTldListICANN();
@@ -40,12 +45,20 @@ final readonly class UpdateRdapServersHandler
$throws[] = $throwable;
}
/*
* Finally, we take the list from IANA and import it again to allow the classification of the latest types of TLDs.
*/
try {
$this->RDAPService->updateRDAPServersFromIANA();
} catch (\Throwable $throwable) {
$throws[] = $throwable;
}
/*
* If it exists, the list of custom RDAP servers is updated at this time.
*/
try {
$this->RDAPService->updateRDAPServersFromFile($this->bag->get('custom_rdap_servers_file'));
} catch (\Throwable $throwable) {

View File

@@ -22,52 +22,54 @@ readonly class ChatNotificationService
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());
if (null === $webhookDsn || 0 === count($webhookDsn)) {
return;
}
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');
}
$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());
}
$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());
}
}
}